ReviewService 0.2.0-dev.45

This is a prerelease version of ReviewService.
There is a newer version of this package available.
See the version list below for details.
dotnet add package ReviewService --version 0.2.0-dev.45                
NuGet\Install-Package ReviewService -Version 0.2.0-dev.45                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ReviewService" Version="0.2.0-dev.45" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ReviewService --version 0.2.0-dev.45                
#r "nuget: ReviewService, 0.2.0-dev.45"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install ReviewService as a Cake Addin
#addin nuget:?package=ReviewService&version=0.2.0-dev.45&prerelease

// Install ReviewService as a Cake Tool
#tool nuget:?package=ReviewService&version=0.2.0-dev.45&prerelease                

Review Service

This repository introduces abstractions around native review capabilities to ease code sharing and testability. It also introduces business logic to quickly configure conditions and state tracking to prompt for reviews at the right moment.

License Version Downloads

Before Getting Started

Before getting started, please read the Android and iOS application review documentation.

Getting Started

  1. Add the ReviewService and ReviewSerivce.NativePrompters NuGet packages to your projects (Windows, Android and iOS).

    💡 If you need to implement more platforms or create custom implementations, you can use the ReviewService.Abstractions NuGet package.

  2. Create an instance of ReviewService. We'll cover dependency injection in details later on in this documentation.

    using ReviewService;
    
    var reviewConditionsBuilder = ReviewConditionsBuilder.Empty()
       .MinimumPrimaryActionsCompleted(1);
    
    var reviewService = new ReviewService<ReviewSettings>(
        logger: null,
        reviewPrompter: new ReviewPrompter(logger: null),
        reviewSettingsSource: new MemoryReviewSettingsSource(),
        reviewConditionsBuilder: reviewConditionsBuilder
    )
    
  3. Use the service.

    • Update the review settings based on application events.
      using ReviewService;
      
      private readonly IReviewService<ReviewSettings> _reviewService;
      
      public async Task DoPrimaryAction(CancellationToken ct)
      {
           // Do Primary Action.
      
           // Track this action.
           await _reviewService.TrackPrimaryActionCompleted(ct)
      }
      
    • Use the service to request review.
      using ReviewService;
      
      private readonly IReviewService<ReviewSettings> _reviewService;
      
      public async Task OnCompletedImportantFlow(CancellationToken ct)
      {
           // Do Meaningful Task.
      
           // Check if all conditions are satisfied and prompt for review if they are.
           await _reviewService.TryRequestReview(ct);
      }
      

    <img src="docs/review_prompt_android.gif" alt="Review Prompt Android" width="250"> <img src="docs/review_prompt_ios.gif" alt="Review Prompt iOS" width="250">

Next Steps

Persisting Review Settings

MemoryReviewSettingsSource is great for automated testing but should not be the implementation of choice for real use-cases. Instead, you should create your own implementation that persists data on the device (so that review settings don't reset when you kill the app).

using ReviewService;

/// <summary>
/// Storage implementation of <see cref="IReviewSettingsSource{TReviewSettings}"/>.
/// </summary>
/// <typeparam name="TReviewSettings">The type of the persisted object.</typeparam>
public sealed class StorageReviewSettingsSource<TReviewSettings> : IReviewSettingsSource<TReviewSettings>
    where TReviewSettings : ReviewSettings
{
    /// <inheritdoc/>
    public Task<TReviewSettings> Read(CancellationToken ct)
    {
        // TODO: Return stored review settings.
    }

    /// <inheritdoc/>
    public Task Write(CancellationToken ct, TReviewSettings reviewSettings)
    {
        // TODO: Update stored review settings.
    }
}

Using Dependency Injection

Here is a simple code that does dependency injection using Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ReviewService;

var host = new HostBuilder()
    .ConfigureServices(serviceCollection => serviceCollection
        .AddSingleton<IReviewPrompter, ReviewPrompter>()
        .AddSingleton<IReviewSettingsSource<ReviewSettingsCustom>, ReviewSettingsSource>()
        .AddTransient(s => ReviewConditionsBuilder
            .Default<ReviewSettingsCustom>()
        )
        .AddSingleton<IReviewService<ReviewSettingsCustom>, ReviewService<ReviewSettingsCustom>>()
    )
    .Build();

💡 We recommend that you define your own interface that wraps IReviewService<YouChoiceOfReviewSettings> to make the usage code leaner and ease any potential refactorings.

/// <summary>
/// This interface wraps <see cref="IReviewService{TReviewSettings}"/> so that you don't have to repeat the generic parameter everywhere that you would use the review service.
/// In other words, you should use this interface in the app instead of <see cref="IReviewService{TReviewSettings}"/> because it's leaner.
/// </summary>
/// <remarks>
/// If you would change <see cref="ReviewSettings"/> for a custom type, using this interface allows you to minimize any refactoring effort by limiting it to this interface and the associated adapter.
/// </remarks>
public interface IReviewService : IReviewService<ReviewSettings>
{
}

Here's a full example.

public static class ReviewConfiguration
{
  public static IServiceCollection AddReviewServices(this IServiceCollection services)
  {
  	return services
  		.AddTransient(s => ReviewConditionsBuilder
  			.Empty()
  			.MinimumPrimaryActionsCompleted(3)
  		)
  		.AddSingleton<IReviewPrompter, LoggingReviewPrompter>()
  		.AddSingleton<IReviewSettingsSource<ReviewSettings>, MemoryReviewSettingsSource<ReviewSettings>>()
  		.AddSingleton<IReviewService<ReviewSettings>, ReviewService<ReviewSettings>>()
  		.AddSingleton<IReviewService, ReviewServiceAdapter>();
  }

  private sealed class ReviewServiceAdapter : IReviewService
  {
  	private readonly IReviewService<ReviewSettings> _reviewService;

  	public ReviewServiceAdapter(IReviewService<ReviewSettings> reviewService)
  	{
  		_reviewService = reviewService;
  	}

  	public Task<bool> GetAreConditionsSatisfied(CancellationToken ct) => _reviewService.GetAreConditionsSatisfied(ct);

  	public Task TryRequestReview(CancellationToken ct) => _reviewService.TryRequestReview(ct);

  	public Task UpdateReviewSettings(CancellationToken ct, Func<ReviewSettings, ReviewSettings> updateFunction) => _reviewService.UpdateReviewSettings(ct, updateFunction);

Features

Now that everything is setup, Let's see what else we can do!

Tack Application Events

To track the provided review settings you can use the following IReviewService extensions.

💡 The review request count and the last review request are automatically tracked by the service.

Customize Tracking Data

If you need custom conditions for your application, you have to create another record that inherits from ReviewSettings.

using ReviewService;

/// <summary>
/// The custom review prompt settings used for prompt conditions.
/// </summary>
public record ReviewSettingsCustom : ReviewSettings
{
   /// <summary>
   /// Gets or sets if the application onboarding has been completed.
   /// </summary>
   public bool HasCompletedOnboarding { get; init; }
}

Add Tracking for Custom Application Events

To track your custom review settings, you can create extensions for IReviewService and be sure to make them generic so they are usable with custom review settings.

using ReviewService;

/// <summary>
/// Extensions of <see cref="IReviewService{TReviewSettings}"/>.
/// </summary>
public static class ReviewServiceExtensions
{
    /// <summary>
    /// Tracks that the application onboarding has been completed.
    /// </summary>
    /// <typeparam name="TReviewSettings">The type of the object that we use for tracking.</typeparam>
    /// <param name="reviewService"><see cref="IReviewService{TReviewSettings}"/>.</param>
    /// <param name="ct">The cancellation token.</param>
    /// <returns><see cref="Task"/>.</returns>
    public static async Task TrackOnboardingCompleted<TReviewSettings>(this IReviewService<TReviewSettings> reviewService, CancellationToken ct)
        where TReviewSettings : ReviewSettingsCustom
    {
        await reviewService.UpdateReviewSettings(ct, reviewSettings =>
        {
            return reviewSettings with { HasCompletedOnboarding = reviewSettings.HasCompletedOnboarding };
        });
    }
}
Built-in Tracking Data

Configure Conditions

If you want to use our default review conditions, you can use ReviewConditionsBuilder.Default() and pass it to the ReviewService constructor, or register it as a transient dependency when using dependency injection. Please note that our review conditions are also generic, so they can be used with custom review settings too.

The ReviewConditionsBuilder.Default() extension method uses the following conditions.

  • 3 application launches required.
  • 2 completed primary actions.
  • 5 days since the first application launch.
  • 15 days since the last review request.
Built-in Conditions

Add Custom Conditions

To create custom review conditions, you have to use ReviewConditionsBuilder.Custom and ReviewConditionsBuilder.CustomAsync and provide them with a function directly instead of a condition. Also you can create extensions for IReviewConditionsBuilder and add a new condition to the builder. To create a review condition, you can use both SynchronousReviewCondition and AsynchronousReviewCondition you need to provide them with a function.

namespace ReviewService;

/// <summary>
/// Extensions for <see cref="IReviewConditionsBuilder{TReviewSettings}"/>.
/// </summary>
public static partial class ReviewConditionsBuilderExtensions
{
    /// <summary>
    /// The application onboarding must be completed.
    /// </summary>
    /// <typeparam name="TReviewSettings">The type of the object that we use for tracking.</typeparam>
    /// <param name="builder">The builder.</param>
    /// <returns><see cref="IReviewConditionsBuilder{TReviewSettings}"/>.</returns>
    public static IReviewConditionsBuilder<TReviewSettings> ApplicationOnboardingCompleted<TReviewSettings>(this IReviewConditionsBuilder<TReviewSettings> builder)
        where TReviewSettings : ReviewSettingsCustom
    {
        builder.Conditions.Add(new SynchronousReviewCondition<TReviewSettings>(
            (reviewSettings, currentDateTime) => reviewSettings.HasCompletedOnboarding is true)
        );
        return builder;
    }
}

Here is a simple code that uses the builder extensions for review conditions.

var reviewConditionsBuilder = ReviewConditionsBuilder.Empty()
   .MinimumPrimaryActionsCompleted(1)
   .MinimumSecondaryActionsCompleted(1)
   .MinimumApplicationLaunchCount(1)
   .MinimumTimeElapsedSinceApplicationFirstLaunch(TimeSpan.FromDays(1))
   .Custom((reviewSettings, currentDateTime) =>
   {
       return reviewSettings.PrimaryActionCompletedCount + reviewSettings.SecondaryActionCompletedCount >= 2;
   });

It's possible to customize the review conditions used by the service by using ReviewConditionsBuilder and passing it to the ReviewService constructor or by injecting it as a transient when using dependency injection.

using ReviewService;

var reviewConditionsBuilder = ReviewConditionsBuilder.Empty<ReviewSettingsCustom>()
    .ApplicationOnboardingCompleted()
    .MinimumPrimaryActionsCompleted(3)
    .MinimumApplicationLaunchCount(3)
    .MinimumTimeElapsedSinceApplicationFirstLaunch(TimeSpan.FromDays(5))
    .Custom((reviewSettings, currentDateTime) =>
    {
        return reviewSettings.PrimaryActionCompletedCount + reviewSettings.SecondaryActionCompletedCount >= 2;
    });

Testing

This is what you need to know before testing and debugging this service. Please note that this may change and you should always refer to the Apple and Android documentation for the most up-to-date information.

Android

  • You can't test this service while debugging the application, the prompt won't show up. To test it, you need to use the internal application sharing or the internal testing feature in Google Play Console. See this for more details.
  • You can't use a Google Suite account on Google Play to review an application because the prompt will not show up.

iOS

  • You can test on a real device or on a simulator.
  • You can test this service only while debugging the application (It won't show up on TestFlight).

Acknowledgements

Take a look at StoreReviewPlugin that we use to prompt for review.

Breaking Changes

Please consult BREAKING_CHANGES.md for more information about version history and compatibility.

License

This project is licensed under the Apache 2.0 license - see the LICENSE file for details.

Contributing

Please read CONTRIBUTING.md for details on the process for contributing to this project.

Be mindful of our Code of Conduct.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 14,355 7/20/2023
0.2.0-dev.45 137 7/19/2023
0.2.0-dev.43 90 7/19/2023
0.2.0-dev.39 90 7/18/2023
0.1.0-dev.37 94 7/14/2023