Skip to content

Instantly share code, notes, and snippets.

@venbrinoDev
Last active April 17, 2024 08:00
Show Gist options
  • Save venbrinoDev/ae536e84a05fe2d53a8849c99419133b to your computer and use it in GitHub Desktop.
Save venbrinoDev/ae536e84a05fe2d53a8849c99419133b to your computer and use it in GitHub Desktop.

App review - spawn a review only when user meets a threashold

Still Testing

import 'dart:math';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:zylag/core/data/local_data_source.dart';
import 'package:zylag/core/data/typedefs.dart';
import 'package:zylag/core/service/AppReview/model/review.dart';




///use a service locator to register
///[LocalStorageService] can be your own local storage
final appReview = AppReview(
    localStorageService: LocalStorageService(),
    inAppReview: InAppReview.instance);



void main() {
  ///you can customize the threadhold calculation
  ///you can skip this to use default threadhold calculation
  appReview.setThresholdAlgorithm((currentThreshold) {
    return currentThreshold! * 2;
  });

  ///Start the start of your app
  ///You can call this at the start of your app
  appReview.startReviewEngine();

  ///Call this anywhere in your app to try and spwan a review 
  appReview.tryReview();
}


typedef ThresholdAlgorithm = int Function(int? currentThreshold);

class AppReview {
  final LocalStorageService localStorageService;

  final InAppReview inAppReview;

  ///This is the default threashold,
  ///you can updated yours as you want
  final int defaultThreshold;

  ThresholdAlgorithm? thresholdAlgorithm;
  ReviewData? _reviewData;

  AppReview({
    required this.localStorageService,
    required this.inAppReview,
    this.defaultThreshold = 2,
  });

  ///The data containg all the review data
  ReviewData? get data => _reviewData;

  ///The current threashold to be met before a review can be spawn
  int? get threshold => _reviewData?.threshold;

  ///How many times users have launch the app
  ///
  ///This reset after a review has been spawned
  int? get launchCount => _reviewData?.launchCount;

  ///Start the app review engine
  Future<void> startReviewEngine({bool shouldRegister = true}) async {
    final data = await localStorageService
        .readJsonData(dotenv.get('APP_REVIEW_KEY')) as DynamicMap?;

    if (data != null) {
      _reviewData = ReviewData.fromJson(data);
    }

    if (shouldRegister) {
      await _registerStatistics();
    }
  }

  ///Allow you set your own threshold algorithm
  /// ```dart
  /// appReview.setThresholdAlgorithm((currentThreshold) {
  ///   return currentThreshold! * 2;
  /// });
  ///
  /// This threashold Algorithm make user use the app 
  /// 2 times the previous use time before spwaing a review
  ///  ````
  void setThresholdAlgorithm(ThresholdAlgorithm? algorithm) {
    thresholdAlgorithm = algorithm;
  }

  ///Try spawing a review
  ///if criteria is meet the review is showned
  Future<void> tryReview() async {
    if (launchCount == null || threshold == null) {
      return;
    }

    if (launchCount! >= threshold!) {
      if (await inAppReview.isAvailable()) {
        await inAppReview.requestReview();
        _updateReviewData();
      }
    }
  }

  ///This updated the review data and caculate the new threshold
  void _updateReviewData() {
    _reviewData = _reviewData?.copyWith(
      launchCount: 0,
      threshold: _calculateThreshold(),
    );
    _save(_reviewData!.toJson());
  }

  int _calculateThreshold() {
    return thresholdAlgorithm?.call(threshold) ?? _defaultAlgorithm(threshold!);
  }

  ///The default alorithm
  ///
  ///Allows user experience your app twice the threashold before spwing a review
  ///
  ///The more they use the longer it takes to ask for a review
  int _defaultAlgorithm(int threshold) => pow(threshold, 2).toInt();


  ///Register the lanuch count
  Future<void> _registerStatistics() async {
    if (_reviewData != null && _reviewData!.canRegister) {
      if (launchCount! < threshold!) {
        _reviewData = _reviewData?.copyWith(
          launchCount: _reviewData!.launchCount! + 1,
        );
        await _save(_reviewData!.toJson());
      }
    } else {
      await _createNewData();
    }
  }

  Future<void> _createNewData() async {
    _reviewData = ReviewData(
      launchCount: 1,
      threshold: defaultThreshold < 2 ? 2 : defaultThreshold,
    );
    await _save(_reviewData!.toJson());
  }

  Future<void> _save(DynamicMap json) async {
    await localStorageService.createJsonData(
      dotenv.get('APP_REVIEW_KEY'),
      json,
    );
  }

  ///Delete all the data and start again
  Future<void> deleteData() async {
    await localStorageService.delete(dotenv.get('APP_REVIEW_KEY'));
  }
}

Review Data Model

import 'package:equatable/equatable.dart';

class ReviewData extends Equatable {
  final int? launchCount;
  final int? threshold;

  const ReviewData({this.launchCount, this.threshold});

  bool get canRegister => threshold != null && launchCount != null;

  ReviewData copyWith(
          {bool? hasReviewed,
          bool? canReviwed,
          int? launchCount,
          int? threshold}) =>
      ReviewData(
        launchCount: launchCount ?? this.launchCount,
        threshold: threshold ?? this.threshold,
      );

  factory ReviewData.fromJson(Map<String, dynamic> json) => ReviewData(
        launchCount: json['launchCount'],
        threshold: json['threshold'],
      );

  Map<String, dynamic> toJson() => {
        'launchCount': launchCount,
        'threshold': threshold,
      };

  @override
  List<Object?> get props => [launchCount, threshold];
}

Test for App review

import 'dart:io';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:zylag/core/service/AppReview/app_review.dart';

import '../../../mock.dart';

void main() {
  group('App Review', () {
    late MockLocalStorage mockLocalStorageService;
    late MockInAppReview mockInAppReview;

    setUp(() {
      mockLocalStorageService = MockLocalStorage();
      mockInAppReview = MockInAppReview();
    });

    setUpAll(() {
      dotenv.testLoad(fileInput: File('.env').readAsStringSync());
    });

    test(
        'AppReview - startReviewEngine - initializes data and registers if needed',
        () async {
      // Set expectations for LocalStorageService
      when(() => mockLocalStorageService.readJsonData(any()))
          .thenAnswer((_) async => null);

      when(() => mockLocalStorageService.createJsonData(any(), any()))
          .thenAnswer((_) async => {});

      // Create an AppReview instance
      final appReview = AppReview(
        localStorageService: mockLocalStorageService,
        inAppReview: mockInAppReview,
      );

      // Call the method
      await appReview.startReviewEngine();

      // Verify interactions with LocalStorageService
      verify(() => mockLocalStorageService.readJsonData(any())).called(1);

      verify(() => mockLocalStorageService.createJsonData(
            any(),
            any(),
          )).called(1);
    });

    test(
        'AppReview - tryReview - requests review if criteria is met and InAppReview is available',
        () async {
      // Set expectations for LocalStorageService and InAppReview
      when(() =>
          mockLocalStorageService
              .readJsonData(dotenv.get('APP_REVIEW_KEY'))).thenAnswer((_) => {
            'launchCount': 3,
            'threshold': 2,
          });
      when(() => mockInAppReview.isAvailable()).thenAnswer((_) async => true);

      // Create an AppReview instance
      final appReview = AppReview(
        localStorageService: mockLocalStorageService,
        inAppReview: mockInAppReview,
      );

      // Call the method
      await appReview.tryReview();

      // Verify interactions with LocalStorageService and InAppReview
      verifyNever(() => mockInAppReview.isAvailable());
      verifyNever(() => mockInAppReview.requestReview());
    });

    test(
        'AppReview - setThresholdAlgorithm - sets the custom threshold algorithm',
        () {
      // Create an AppReview instance
      final appReview = AppReview(
        localStorageService: mockLocalStorageService,
        inAppReview: mockInAppReview,
      );

      // Define a custom threshold algorithm
      int customThreshold(int? currentThreshold) => currentThreshold! * 3;

      // Set the custom algorithm
      appReview.setThresholdAlgorithm(customThreshold);

      // Verify that the custom algorithm is set
      expect(appReview.thresholdAlgorithm, customThreshold);
    });

    test('AppReview  - uses custom algorithm if set', () async {
      // Set expectations for LocalStorageService
      when(() => mockLocalStorageService.readJsonData(any()))
          .thenAnswer((_) async => {
                'threshold': 2,
                'launchCount': 1,
              });

      when(() => mockInAppReview.isAvailable()).thenAnswer((_) async => true);

      // Create an AppReview instance
      final appReview = AppReview(
        localStorageService: mockLocalStorageService,
        inAppReview: mockInAppReview,
      );

      when(() => mockLocalStorageService.createJsonData(any(), any()))
          .thenAnswer((_) async => appReview.data?.toJson());

      // Define a custom threshold algorithm
      int customThreshold(int? currentThreshold) => currentThreshold! * 3;

      // Set the custom algorithm
      appReview.setThresholdAlgorithm(customThreshold);

      await appReview.startReviewEngine();

      await appReview.tryReview();

      // Verify that the custom algorithm was used
      expect(appReview.threshold, 6);
    });

    test('AppReview - - uses default algorithm if custom not set', () async {
      // Set expectations for LocalStorageService
      when(() => mockLocalStorageService.readJsonData(any()))
          .thenAnswer((_) async => {
                'threshold': 2,
                'launchCount': 1,
              });

      when(() => mockInAppReview.isAvailable()).thenAnswer((_) async => true);

      // Create an AppReview instance
      final appReview = AppReview(
        localStorageService: mockLocalStorageService,
        inAppReview: mockInAppReview,
      );

      when(() => mockLocalStorageService.createJsonData(any(), any()))
          .thenAnswer((_) async => appReview.data?.toJson());

      await appReview.startReviewEngine();

      await appReview.tryReview();

      // Verify that the custom algorithm was used
      expect(appReview.threshold, 4);
    });
  });

  // Add more tests for other methods of AppReview class...
}

Mock

import 'package:in_app_review/in_app_review.dart';
import 'package:mocktail/mocktail.dart';
import 'package:zylag/core/data/local_data_source.dart';

class MockLocalStorage extends Mock implements LocalStorageService {
  MockLocalStorage() {
    when(() => delete(any())).thenAnswer((_) async {});
  }
}

class MockInAppReview extends Mock implements InAppReview {
  MockInAppReview() {
    when(() => requestReview()).thenAnswer((_) async {});
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment