Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Port code to dart #4

Merged
merged 3 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ steps:
- task: gitversion/execute@0
inputs:
useConfigFile: true
configFilePath: $(Build.SourcesDirectory)/build/gitversion-config.yml
configFilePath: .azuredevops/gitversion-config.yml
displayName: 'Calculate App Version'

- task: PowerShell@2
Expand Down
File renamed without changes.
File renamed without changes.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ migrate_working_dir/
.packages
.pub-cache/
.pub/
build/
pubspec.lock

# Web related
Expand All @@ -40,4 +41,7 @@ app.*.symbols
app.*.map.json

# Test related
coverage
coverage

# Mockito generated files.
*.mocks.dart
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
## 0.3.0

- Migrated the pipeline from the .NET version of the package.
- Ported the code from the .NET version of the package to dart.
365 changes: 344 additions & 21 deletions README.md

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions lib/review_service.dart

This file was deleted.

7 changes: 0 additions & 7 deletions lib/src/review_service.dart

This file was deleted.

15 changes: 15 additions & 0 deletions lib/src/review_service/asynchronous_review_condition.dart
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);
}
}
15 changes: 15 additions & 0 deletions lib/src/review_service/logging_review_prompter.dart
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.');
}
}
23 changes: 23 additions & 0 deletions lib/src/review_service/memory_review_settings_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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> {
late TReviewSettings _reviewSettings;

MemoryReviewSettingsSource(TReviewSettings Function() defaultSettings) {
_reviewSettings = defaultSettings();
}

@override
Future<TReviewSettings> read() {
return Future.value(_reviewSettings);
}

@override
Future<void> write(TReviewSettings reviewSettings) {
_reviewSettings = reviewSettings;

return Future.value();
}
}
10 changes: 10 additions & 0 deletions lib/src/review_service/review_condition.dart
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,
);
}
35 changes: 35 additions & 0 deletions lib/src/review_service/review_condition_builder.dart
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 lib/src/review_service/review_conditions_builder.extensions.dart
Lee31416 marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
35 changes: 35 additions & 0 deletions lib/src/review_service/review_prompter.dart
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.');
}
}
}
103 changes: 103 additions & 0 deletions lib/src/review_service/review_service.dart
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 {
Lee31416 marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
}
34 changes: 34 additions & 0 deletions lib/src/review_service/review_service.extensions.dart
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(
Lee31416 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Lee31416 marked this conversation as resolved.
Show resolved Hide resolved
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,
);
}
}
Loading
Loading