generated from nventive/Template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
823 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import 'package:review_service/src/review_service/review_condition.dart'; | ||
import 'package:review_service/src/review_service/review_settings.dart'; | ||
|
||
/// Asynchronous implementation of [ReviewCondition]. | ||
class AsynchronousReviewCondition<TReviewSettings extends ReviewSettings> | ||
implements ReviewCondition<TReviewSettings> { | ||
final Future<bool> Function(TReviewSettings, DateTime) _condition; | ||
|
||
AsynchronousReviewCondition(this._condition); | ||
|
||
@override | ||
Future<bool> validate(TReviewSettings currentSettings, DateTime currentDateTime) { | ||
return _condition(currentSettings, currentDateTime); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import 'package:logger/logger.dart'; | ||
import 'package:review_service/src/review_service/review_prompter.dart'; | ||
|
||
/// Implementation of [ReviewPrompter] that logs information. | ||
final class LoggingReviewPrompter implements ReviewPrompter { | ||
final Logger logger; | ||
|
||
/// Initializes a new instance of the <see cref="LoggingReviewPrompter"/> class. | ||
LoggingReviewPrompter({Logger? logger}) : logger = logger ?? Logger(); | ||
|
||
@override | ||
Future<void> tryPrompt() async { | ||
logger.i('TryPrompt was invoked.'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import 'package:review_service/src/review_service/review_settings.dart'; | ||
import 'package:review_service/src/review_service/review_settings_source.dart'; | ||
|
||
/// In-memory implementation of [ReviewSettingsSource]. | ||
final class MemoryReviewSettingsSource<TReviewSettings extends ReviewSettings> implements ReviewSettingsSource<TReviewSettings> { | ||
TReviewSettings _reviewSettings = const ReviewSettings() as TReviewSettings; | ||
|
||
@override | ||
Future<TReviewSettings> read() { | ||
return Future.value(_reviewSettings); | ||
} | ||
|
||
@override | ||
Future<void> write(TReviewSettings reviewSettings) { | ||
_reviewSettings = reviewSettings; | ||
|
||
return Future.value(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import 'package:review_service/src/review_service/review_settings.dart'; | ||
|
||
/// A condition used to determine if a review should be requested based on [ReviewSettings]. | ||
abstract interface class ReviewCondition<TReviewSettings extends ReviewSettings> { | ||
/// Validates that the condition is satisfied. | ||
Future<bool> validate( | ||
TReviewSettings currentSettings, | ||
DateTime currentDateTime, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import 'package:review_service/src/review_service/review_condition.dart'; | ||
import 'package:review_service/src/review_service/review_conditions_builder.extensions.dart'; | ||
import 'package:review_service/src/review_service/review_settings.dart'; | ||
|
||
/// Provide a way to gather the review conditions. | ||
abstract interface class ReviewConditionsBuilder< | ||
TReviewSettings extends ReviewSettings> { | ||
/// Gets the review conditions. | ||
List<ReviewCondition<TReviewSettings>> get conditions; | ||
} | ||
|
||
/// Implementation of [ReviewConditionsBuilder]. | ||
/// Provides methods to get an empty or default [ReviewConditionsBuilder]. | ||
final class ReviewConditionsBuilderImplementation< | ||
TReviewSettings extends ReviewSettings> | ||
implements ReviewConditionsBuilder<TReviewSettings> { | ||
ReviewConditionsBuilderImplementation(); | ||
|
||
@override | ||
List<ReviewCondition<TReviewSettings>> conditions = []; | ||
|
||
static ReviewConditionsBuilder<TReviewSettings> | ||
empty<TReviewSettings extends ReviewSettings>() { | ||
return ReviewConditionsBuilderImplementation<TReviewSettings>(); | ||
} | ||
|
||
static ReviewConditionsBuilder<TReviewSettings> | ||
defaultBuilder<TReviewSettings extends ReviewSettings>() { | ||
return ReviewConditionsBuilderImplementation<TReviewSettings>() | ||
.minimumApplicationLaunchCount(3) | ||
.minimumElapsedTimeSinceApplicationFirstLaunch(const Duration(days: 5)) | ||
.minimumPrimaryActionsCompleted(2) | ||
.minimumElapsedTimeSinceLastReviewRequest(const Duration(days: 15)); | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
lib/src/review_service/review_conditions_builder.extensions.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
|
||
import 'package:review_service/src/review_service/asynchronous_review_condition.dart'; | ||
import 'package:review_service/src/review_service/review_condition_builder.dart'; | ||
import 'package:review_service/src/review_service/review_settings.dart'; | ||
import 'package:review_service/src/review_service/synchronous_review_condition.dart'; | ||
|
||
/// Extensions for [ReviewConditionsBuilder]. | ||
extension ReviewConditionBuilderExtensions<TReviewSettings extends ReviewSettings> | ||
on ReviewConditionsBuilder<TReviewSettings> { | ||
|
||
/// The number of completed primary actions must be greater than the review settings. | ||
ReviewConditionsBuilder<TReviewSettings> minimumPrimaryActionsCompleted(int minimumActionCompleted) { | ||
conditions.add(SynchronousReviewCondition<TReviewSettings>( | ||
(reviewSettings, currentDateTime) => reviewSettings.primaryActionCompletedCount >= minimumActionCompleted, | ||
)); | ||
return this; | ||
} | ||
|
||
/// The number of completed secondary actions must be greater than the review settings. | ||
ReviewConditionsBuilder<TReviewSettings> minimumSecondaryActionsCompleted(int minimumActionCompleted) { | ||
conditions.add(SynchronousReviewCondition<TReviewSettings>( | ||
(reviewSettings, currentDateTime) => reviewSettings.secondaryActionCompletedCount >= minimumActionCompleted, | ||
)); | ||
return this; | ||
} | ||
|
||
/// The number of times the application has been launched must be greater than the review settings. | ||
ReviewConditionsBuilder<TReviewSettings> minimumApplicationLaunchCount(int minimumCount) { | ||
conditions.add(SynchronousReviewCondition<TReviewSettings>( | ||
(reviewSettings, currentDateTime) => reviewSettings.applicationLaunchCount >= minimumCount, | ||
)); | ||
return this; | ||
} | ||
|
||
/// The elapsed time since the first application launch must be greater than the review settings. | ||
ReviewConditionsBuilder<TReviewSettings> minimumElapsedTimeSinceApplicationFirstLaunch(Duration minimumTimeElapsed) { | ||
conditions.add(SynchronousReviewCondition<TReviewSettings>( | ||
(reviewSettings, currentDateTime) => reviewSettings.firstApplicationLaunch != null && | ||
reviewSettings.firstApplicationLaunch!.add(minimumTimeElapsed).isBefore(currentDateTime), | ||
)); | ||
return this; | ||
} | ||
|
||
/// The time elapsed since the last review requested must be greater than the review settings. | ||
ReviewConditionsBuilder<TReviewSettings> minimumElapsedTimeSinceLastReviewRequest(Duration minimumTimeElapsed) { | ||
conditions.add(SynchronousReviewCondition<TReviewSettings>( | ||
(reviewSettings, currentDateTime) => reviewSettings.lastRequest == null || | ||
reviewSettings.lastRequest!.add(minimumTimeElapsed).isBefore(currentDateTime), | ||
)); | ||
return this; | ||
} | ||
|
||
/// Adds a custom synchronous condition. | ||
ReviewConditionsBuilder<TReviewSettings> custom(bool Function(TReviewSettings, DateTime) condition) { | ||
conditions.add(SynchronousReviewCondition<TReviewSettings>(condition)); | ||
return this; | ||
} | ||
|
||
/// Adds a custom asynchronous condition. | ||
ReviewConditionsBuilder<TReviewSettings> customAsync( | ||
Future<bool> Function(TReviewSettings, DateTime) condition) { | ||
conditions.add(AsynchronousReviewCondition<TReviewSettings>(condition)); | ||
return this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import 'package:in_app_review/in_app_review.dart'; | ||
import 'package:logger/logger.dart'; | ||
|
||
/// Provides ways to prompt user to review the current application. | ||
abstract interface class ReviewPrompter { | ||
factory ReviewPrompter({ | ||
required Logger logger, | ||
}) = _ReviewPrompter; | ||
|
||
/// Prompts the user to rate the current application using the platform's default application store. | ||
Future<void> tryPrompt(); | ||
} | ||
|
||
/// Implementation of [ReviewPrompter]. | ||
final class _ReviewPrompter implements ReviewPrompter { | ||
final Logger _logger; | ||
|
||
_ReviewPrompter({ | ||
required Logger logger, | ||
}) : _logger = logger; | ||
|
||
@override | ||
Future<void> tryPrompt() async { | ||
_logger.d('Trying to prompt user to review the current application.'); | ||
|
||
if (await InAppReview.instance.isAvailable()) { | ||
InAppReview.instance.requestReview(); | ||
|
||
_logger.i('Prompted user to review the current application.'); | ||
} else { | ||
_logger.i( | ||
'Failed to prompt user to review the current application because the platform does not support it.'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import 'package:logger/logger.dart'; | ||
import 'package:review_service/src/review_service/review_condition.dart'; | ||
import 'package:review_service/src/review_service/review_condition_builder.dart'; | ||
import 'package:review_service/src/review_service/review_prompter.dart'; | ||
import 'package:review_service/src/review_service/review_settings.dart'; | ||
import 'package:review_service/src/review_service/review_settings_source.dart'; | ||
|
||
/// Provides ways to validate review prompt conditions using [TReviewSettings] and | ||
/// to prompt user to review the current application using [ReviewPrompter]. | ||
abstract interface class ReviewService<TReviewSettings extends ReviewSettings> { | ||
factory ReviewService({ | ||
required Logger logger, | ||
required ReviewPrompter reviewPrompter, | ||
required ReviewSettingsSource<TReviewSettings> reviewSettingsSource, | ||
required ReviewConditionsBuilder<TReviewSettings> reviewConditionsBuilder, | ||
}) = _ReviewService; | ||
|
||
/// Checks if all review prompt conditions are satisfied and then prompt user to review the current application. | ||
Future<void> tryRequestReview(); | ||
|
||
/// Gets if all conditions are satisfied which means that we can prompt user to review the current application. | ||
Future<bool> getAreConditionsSatisfied(); | ||
|
||
/// Updates the persisted [TReviewSettings]. | ||
Future<void> updateReviewSettings( | ||
TReviewSettings Function(TReviewSettings) updateFunction, | ||
); | ||
} | ||
|
||
/// Implementation of [IReviewService]. | ||
final class _ReviewService<TReviewSettings extends ReviewSettings> | ||
implements ReviewService<TReviewSettings> { | ||
final Logger _logger; | ||
final ReviewPrompter _reviewPrompter; | ||
final ReviewSettingsSource<TReviewSettings> _reviewSettingsSource; | ||
final List<ReviewCondition<TReviewSettings>> _reviewConditions; | ||
|
||
_ReviewService({ | ||
required Logger logger, | ||
required ReviewPrompter reviewPrompter, | ||
required ReviewSettingsSource<TReviewSettings> reviewSettingsSource, | ||
required ReviewConditionsBuilder<TReviewSettings> reviewConditionsBuilder, | ||
}) : _logger = logger, | ||
_reviewPrompter = reviewPrompter, | ||
_reviewSettingsSource = reviewSettingsSource, | ||
_reviewConditions = reviewConditionsBuilder.conditions; | ||
|
||
/// Tracks that a review was requested. | ||
Future<void> _trackReviewRequested() async { | ||
await updateReviewSettings((reviewSettings) { | ||
return reviewSettings.copyWith( | ||
lastRequest: DateTime.now(), | ||
requestCount: reviewSettings.requestCount + 1, | ||
) as TReviewSettings; | ||
}); | ||
} | ||
|
||
@override | ||
Future<void> tryRequestReview() async { | ||
_logger.d('Trying to request a review.'); | ||
|
||
if (await getAreConditionsSatisfied()) { | ||
await _reviewPrompter.tryPrompt(); | ||
await _trackReviewRequested(); | ||
|
||
_logger.i('Review requested.'); | ||
} else { | ||
_logger.i('Failed to request a review because one or more conditions were not satisfied.'); | ||
} | ||
} | ||
|
||
@override | ||
Future<bool> getAreConditionsSatisfied() async { | ||
_logger.d('Evaluating conditions.'); | ||
|
||
final currentSettings = await _reviewSettingsSource.read(); | ||
final reviewConditionTasks = _reviewConditions.map((condition) => condition.validate(currentSettings, DateTime.now())); | ||
final result = (await Future.wait(reviewConditionTasks)).every((x) => x); | ||
|
||
if (result) { | ||
_logger.i('Evaluated conditions and all conditions are satisfied.'); | ||
} else { | ||
_logger.i('Evaluted conditions and one or more conditions were not satisfied.'); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
@override | ||
Future<void> updateReviewSettings(TReviewSettings Function(TReviewSettings) updateFunction) async { | ||
_logger.d('Updating review settings.'); | ||
|
||
final currentSettings = await _reviewSettingsSource.read(); | ||
|
||
try { | ||
await _reviewSettingsSource.write(updateFunction(currentSettings)); | ||
|
||
_logger.i('Updated review settings.'); | ||
} catch (ex) { | ||
_logger.e('Failed to update review settings.', error: ex); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import 'package:review_service/src/review_service/review_service.dart'; | ||
import 'package:review_service/src/review_service/review_settings.dart'; | ||
|
||
/// Extensions of [IReviewService]. | ||
extension ReviewServiceExtensions<TReviewSettings extends ReviewSettings> on ReviewService<TReviewSettings> { | ||
|
||
/// Tracks that the application was launched. | ||
Future<void> trackApplicationLaunched() async { | ||
await updateReviewSettings((reviewSettings) { | ||
return reviewSettings.firstApplicationLaunch == null | ||
? reviewSettings.copyWith( | ||
firstApplicationLaunch: DateTime.now(), | ||
applicationLaunchCount: reviewSettings.applicationLaunchCount + 1, | ||
) as TReviewSettings | ||
: reviewSettings.copyWith( | ||
applicationLaunchCount: reviewSettings.applicationLaunchCount + 1, | ||
) as TReviewSettings; | ||
}); | ||
} | ||
|
||
/// Tracks that a primary action was completed. | ||
Future<void> trackPrimaryActionCompleted() async { | ||
await updateReviewSettings( | ||
(reviewSettings) => reviewSettings.copyWith(primaryActionCompletedCount: reviewSettings.primaryActionCompletedCount + 1) as TReviewSettings, | ||
); | ||
} | ||
|
||
/// Tracks that a secondary action was completed. | ||
Future<void> trackSecondaryActionCompleted() async { | ||
await updateReviewSettings( | ||
(reviewSettings) => reviewSettings.copyWith(secondaryActionCompletedCount: reviewSettings.secondaryActionCompletedCount + 1) as TReviewSettings, | ||
); | ||
} | ||
} |
Oops, something went wrong.