diff --git a/example/lib/nstack.dart b/example/lib/nstack.dart index 0f1fafe..d4ca9af 100644 --- a/example/lib/nstack.dart +++ b/example/lib/nstack.dart @@ -127,6 +127,55 @@ * `NStackFeatureHandlerWidget` with the `NStackVersionUpdateHandler` config, * and you will get the version update data only once for an app life cycle. * + * ⭐ Rate Reminder + * + * Rate reminders ask the user to rate the app on the platform-specific store once the amount of points from events reaches a certain threshold. + * Use `NStackFeatureHandlerWidget` with `NStackRateReminderHandler` config to integrate the rate reminder feature within your Flutter app. + * And use `postRateReminderEvent`API from `NStackRateReminders` to post points to the backend. + * This setup facilitates different strategies for handling the Rate Reminder + * + * - Default Adaptive Dialog: + * By default, `NStackRateReminderHandler.config()` with `void Function(NStackRateReminderAnswer)` callback is designed to automatically handle rate reminder. + * The callback will provide the user's answer. + * Example: + * + * ```dart + * NStackRateReminderHandler.config( + * onRateReminderAnswered: (rateReminderAnswer) { + * + * }, + * ) + * ``` + * + * - Custom Handling: + * By passing a `void Function(NStackRateReminder)` callback to `NStackRateReminderHandler.config()`, + * you gain control over how the rate reminder alert is presented to the user. In this case, passing the + * `void Function(NStackRateReminderAnswer)` callback is unnecessary. + * Example: + * + * ```dart + * NStackRateReminderHandler.config( + * onRateReminder: (rateReminderInfo) { + * + * }, + * ) + * ``` + * + * Or, if you don't want to use the `NStackFeatureHandlerWidget`, + * you can use the `getRateReminderInfo`, `postRateReminderEvent` and `postRateReminderAnswer` from `NStackRateReminders`. + * Example: + * + * ```dart + * WidgetsBinding.instance.addPostFrameCallback( + * (timeStamp) async { + * final rateReminderInfo = + * await context.nstack.rateReminders.getRateReminderInfo( + * defaultLocale: Localizations.localeOf(context), + * ); + * }, + * ); + * ``` + * * 🛠️ IMPORTANT NOTES FOR SDK USERS * * The default environment for the NStack SDK is `prod`. @@ -151,6 +200,7 @@ import 'package:nstack/src/sdk/localization/section_key_delegate.dart'; import 'package:nstack/src/sdk/widgets/nstack_base_widget.dart'; export 'package:nstack/src/models/app_open_platform.dart'; +export 'package:nstack/src/models/nstack_rate_reminder_answer.dart'; export 'package:nstack/src/models/nstack_version_update_view_request.dart' show NStackVersionUpdateViewAnswer, NStackVersionUpdateViewType; export 'package:nstack/src/sdk/extensions/nstack_widget_extension.dart'; @@ -253,14 +303,14 @@ class NStackWidget extends NStackBaseWidget { required Widget child, AppOpenPlatform? platformOverride, VoidCallback? onComplete, - bool? testMode, + bool testMode = false, }) : super( key: key, child: child, platformOverride: platformOverride, onComplete: onComplete, config: config, - testMode: testMode ?? false, + testMode: testMode, localization: NStackLocalization( config: config, bundledLocalization: BundledLocalizationImpl.data(), diff --git a/example/lib/routes/routes.dart b/example/lib/routes/routes.dart index 11a08ba..5b46aed 100644 --- a/example/lib/routes/routes.dart +++ b/example/lib/routes/routes.dart @@ -16,5 +16,6 @@ class AppRouter extends $AppRouter { AutoRoute(page: MessageExampleRoute.page), AutoRoute(page: VersionUpdateExampleRoute.page), AutoRoute(page: CombineExampleRoute.page), + AutoRoute(page: RateReminderExampleRoute.page), ]; } diff --git a/example/lib/routes/routes.gr.dart b/example/lib/routes/routes.gr.dart index 0bbef2d..1d88cb3 100644 --- a/example/lib/routes/routes.gr.dart +++ b/example/lib/routes/routes.gr.dart @@ -8,39 +8,46 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i5; +import 'package:auto_route/auto_route.dart' as _i6; import 'package:example/screens/combine_example_screen.dart' as _i1; import 'package:example/screens/main_screen.dart' as _i2; import 'package:example/screens/message_example_screen.dart' as _i3; -import 'package:example/screens/version_update_example_screen.dart' as _i4; +import 'package:example/screens/rate_reminder_example.dart' as _i4; +import 'package:example/screens/version_update_example_screen.dart' as _i5; -abstract class $AppRouter extends _i5.RootStackRouter { +abstract class $AppRouter extends _i6.RootStackRouter { $AppRouter({super.navigatorKey}); @override - final Map pagesMap = { + final Map pagesMap = { CombineExampleRoute.name: (routeData) { - return _i5.AutoRoutePage( + return _i6.AutoRoutePage( routeData: routeData, child: const _i1.CombineExampleScreen(), ); }, MainRoute.name: (routeData) { - return _i5.AutoRoutePage( + return _i6.AutoRoutePage( routeData: routeData, child: const _i2.MainScreen(), ); }, MessageExampleRoute.name: (routeData) { - return _i5.AutoRoutePage( + return _i6.AutoRoutePage( routeData: routeData, child: const _i3.MessageExampleScreen(), ); }, + RateReminderExampleRoute.name: (routeData) { + return _i6.AutoRoutePage( + routeData: routeData, + child: const _i4.RateReminderExampleScreen(), + ); + }, VersionUpdateExampleRoute.name: (routeData) { - return _i5.AutoRoutePage( + return _i6.AutoRoutePage( routeData: routeData, - child: const _i4.VersionUpdateExampleScreen(), + child: const _i5.VersionUpdateExampleScreen(), ); }, }; @@ -48,8 +55,8 @@ abstract class $AppRouter extends _i5.RootStackRouter { /// generated route for /// [_i1.CombineExampleScreen] -class CombineExampleRoute extends _i5.PageRouteInfo { - const CombineExampleRoute({List<_i5.PageRouteInfo>? children}) +class CombineExampleRoute extends _i6.PageRouteInfo { + const CombineExampleRoute({List<_i6.PageRouteInfo>? children}) : super( CombineExampleRoute.name, initialChildren: children, @@ -57,13 +64,13 @@ class CombineExampleRoute extends _i5.PageRouteInfo { static const String name = 'CombineExampleRoute'; - static const _i5.PageInfo page = _i5.PageInfo(name); + static const _i6.PageInfo page = _i6.PageInfo(name); } /// generated route for /// [_i2.MainScreen] -class MainRoute extends _i5.PageRouteInfo { - const MainRoute({List<_i5.PageRouteInfo>? children}) +class MainRoute extends _i6.PageRouteInfo { + const MainRoute({List<_i6.PageRouteInfo>? children}) : super( MainRoute.name, initialChildren: children, @@ -71,13 +78,13 @@ class MainRoute extends _i5.PageRouteInfo { static const String name = 'MainRoute'; - static const _i5.PageInfo page = _i5.PageInfo(name); + static const _i6.PageInfo page = _i6.PageInfo(name); } /// generated route for /// [_i3.MessageExampleScreen] -class MessageExampleRoute extends _i5.PageRouteInfo { - const MessageExampleRoute({List<_i5.PageRouteInfo>? children}) +class MessageExampleRoute extends _i6.PageRouteInfo { + const MessageExampleRoute({List<_i6.PageRouteInfo>? children}) : super( MessageExampleRoute.name, initialChildren: children, @@ -85,13 +92,27 @@ class MessageExampleRoute extends _i5.PageRouteInfo { static const String name = 'MessageExampleRoute'; - static const _i5.PageInfo page = _i5.PageInfo(name); + static const _i6.PageInfo page = _i6.PageInfo(name); +} + +/// generated route for +/// [_i4.RateReminderExampleScreen] +class RateReminderExampleRoute extends _i6.PageRouteInfo { + const RateReminderExampleRoute({List<_i6.PageRouteInfo>? children}) + : super( + RateReminderExampleRoute.name, + initialChildren: children, + ); + + static const String name = 'RateReminderExampleRoute'; + + static const _i6.PageInfo page = _i6.PageInfo(name); } /// generated route for -/// [_i4.VersionUpdateExampleScreen] -class VersionUpdateExampleRoute extends _i5.PageRouteInfo { - const VersionUpdateExampleRoute({List<_i5.PageRouteInfo>? children}) +/// [_i5.VersionUpdateExampleScreen] +class VersionUpdateExampleRoute extends _i6.PageRouteInfo { + const VersionUpdateExampleRoute({List<_i6.PageRouteInfo>? children}) : super( VersionUpdateExampleRoute.name, initialChildren: children, @@ -99,5 +120,5 @@ class VersionUpdateExampleRoute extends _i5.PageRouteInfo { static const String name = 'VersionUpdateExampleRoute'; - static const _i5.PageInfo page = _i5.PageInfo(name); + static const _i6.PageInfo page = _i6.PageInfo(name); } diff --git a/example/lib/screens/main_screen.dart b/example/lib/screens/main_screen.dart index 76dee6f..3d2feff 100644 --- a/example/lib/screens/main_screen.dart +++ b/example/lib/screens/main_screen.dart @@ -54,6 +54,14 @@ class MainScreen extends StatelessWidget { 'Combine Example', ), ), + MaterialButton( + onPressed: () => context.pushRoute( + const RateReminderExampleRoute(), + ), + child: const Text( + 'Rate Reminder Example', + ), + ), ], ), ), diff --git a/example/lib/screens/rate_reminder_example.dart b/example/lib/screens/rate_reminder_example.dart new file mode 100644 index 0000000..8096f26 --- /dev/null +++ b/example/lib/screens/rate_reminder_example.dart @@ -0,0 +1,54 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:example/nstack.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +@RoutePage() +class RateReminderExampleScreen extends StatefulWidget { + const RateReminderExampleScreen({super.key}); + + @override + State createState() => + _RateReminderExampleScreenState(); +} + +class _RateReminderExampleScreenState extends State { + @override + Widget build(BuildContext context) { + final localizationAsset = context.localizationAssest; + + return NStackFeatureHandlerWidget( + features: [ + NStackRateReminderHandler.config( + onRateReminderAnswered: (rateReminderAnswer) { + if (kDebugMode) { + print(rateReminderAnswer); + } + }, + ), + ], + child: Scaffold( + appBar: AppBar( + title: Text(localizationAsset.test.testDollarSign), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Rate Reminder Example'), + MaterialButton( + onPressed: () async { + await context.nstack.rateReminders + .postRateReminderEvent(action: 'button-tapped'); + }, + child: const Text( + 'Trigger Points', + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/models/app_open_data.dart b/lib/src/models/app_open_data.dart index 286c31e..f4be3c2 100644 --- a/lib/src/models/app_open_data.dart +++ b/lib/src/models/app_open_data.dart @@ -1,7 +1,6 @@ import 'package:nstack/src/models/localize_index.dart'; import 'package:nstack/src/models/nstack_message.dart'; import 'package:nstack/src/models/nstack_version_update.dart'; -import 'package:nstack/src/models/rate_reminder.dart'; import 'package:nstack/src/models/terms.dart'; import 'package:nstack/src/other/extensions.dart'; @@ -13,7 +12,6 @@ class AppOpenData { final DateTime? createdAt; final DateTime? updatedAt; final NStackMessage? message; - final RateReminder? rateReminder; final List? terms; AppOpenData({ @@ -24,7 +22,6 @@ class AppOpenData { required this.createdAt, required this.updatedAt, required this.message, - required this.rateReminder, required this.terms, }); @@ -48,9 +45,6 @@ class AppOpenData { DateTime.parse, ), message: (json['message'] as Map?)?.let(NStackMessage.fromJson), - rateReminder: json['rateReminder']?.let( - RateReminder.fromJson, - ), terms: json['terms']?.let((item) => item), ); } diff --git a/lib/src/models/nstack_rate_reminder_answer.dart b/lib/src/models/nstack_rate_reminder_answer.dart new file mode 100644 index 0000000..0d138b1 --- /dev/null +++ b/lib/src/models/nstack_rate_reminder_answer.dart @@ -0,0 +1,5 @@ +enum NStackRateReminderAnswer { + positive, + negative, + skip, +} diff --git a/lib/src/models/rate_reminder.dart b/lib/src/models/rate_reminder.dart index b0a53cf..c3ad851 100644 --- a/lib/src/models/rate_reminder.dart +++ b/lib/src/models/rate_reminder.dart @@ -1,28 +1,53 @@ -class RateReminder { - final String? title; - final String? body; - final String? yesButton; - final String? laterButton; - final String? noButton; - final String? link; +class NStackRateReminder { + final int id; + final int pointsToTrigger; + final int daysDelayOnSkip; + final RateReminderLocalization? localization; + final int points; - RateReminder({ + NStackRateReminder({ + required this.id, + required this.pointsToTrigger, + required this.daysDelayOnSkip, + required this.localization, + required this.points, + }); + + factory NStackRateReminder.fromJson(Map json) { + return NStackRateReminder( + id: json['id'], + pointsToTrigger: json['points_to_trigger'], + daysDelayOnSkip: json['days_delay_on_skip'], + localization: RateReminderLocalization.fromJson( + json['localization'] as Map, + ), + points: json['points'], + ); + } +} + +class RateReminderLocalization { + final String title; + final String body; + final String yesBtn; + final String laterBtn; + final String noBtn; + + RateReminderLocalization({ required this.title, required this.body, - required this.yesButton, - required this.laterButton, - required this.noButton, - required this.link, + required this.yesBtn, + required this.laterBtn, + required this.noBtn, }); - factory RateReminder.fromJson(Map json) { - return RateReminder( + factory RateReminderLocalization.fromJson(Map json) { + return RateReminderLocalization( title: json['title'], body: json['body'], - yesButton: json['yesButton'], - laterButton: json['laterButton'], - noButton: json['noButton'], - link: json['link'], + yesBtn: json['yesBtn'], + laterBtn: json['laterBtn'], + noBtn: json['noBtn'], ); } } diff --git a/lib/src/repository/nstack_repository.dart b/lib/src/repository/nstack_repository.dart index 626452f..4e74e4d 100644 --- a/lib/src/repository/nstack_repository.dart +++ b/lib/src/repository/nstack_repository.dart @@ -7,9 +7,9 @@ import 'package:nstack/src/models/app_open_platform.dart'; import 'package:nstack/src/models/localize_index.dart'; import 'package:nstack/src/models/nstack_app_info.dart'; import 'package:nstack/src/models/nstack_config.dart'; -import 'package:nstack/src/models/nstack_message.dart'; import 'package:nstack/src/models/nstack_version_update.dart'; import 'package:nstack/src/models/nstack_version_update_view_request.dart'; +import 'package:nstack/src/models/rate_reminder.dart'; import 'package:nstack/src/utils/log_util.dart'; class NStackRepository { @@ -101,39 +101,6 @@ class NStackRepository { return response.body; } - Future getMessage({ - required String acceptHeader, - required NStackAppInfo appInfoData, - required bool devMode, - required bool testMode, - }) async { - var mutableHeaders = {..._headers}; - mutableHeaders['Accept-Language'] = acceptHeader; - - final requestBody = { - 'platform': appInfoData.platform.slug, - 'guid': appInfoData.guid, - 'version': appInfoData.version, - 'old_version': appInfoData.oldVersion, - 'last_updated': appInfoData.lastUpdated, - }; - - try { - final appOpenResponse = await http.post( - Uri.parse('$_baseUrl/open?dev=$devMode&test=$testMode'), - headers: mutableHeaders, - body: requestBody, - ); - - final result = json.decode(appOpenResponse.body); - final appOpen = AppOpen.fromJson(result); - return appOpen.data.message; - } catch (e) { - LogUtil.log(e); - return null; - } - } - Future postMessageSeen({ required NStackAppInfo appInfoData, required int messageId, @@ -203,4 +170,85 @@ class NStackRepository { throw NStackException.updateFailed('Failed to update change log seen.'); } } + + Future getRateReminderInfo({ + required String acceptHeader, + required NStackAppInfo appInfoData, + }) async { + var mutableHeaders = {..._headers}; + mutableHeaders['Accept-Language'] = acceptHeader; + try { + final rateReminderResponse = await http.get( + Uri.parse( + '$_baseUrl/notify/rate_reminder_v2?guid=${appInfoData.guid}', + ), + headers: mutableHeaders, + ); + + final result = json.decode(rateReminderResponse.body); + + // Log rate reminder skip message + if (rateReminderResponse.statusCode == 445) { + LogUtil.log('Rate Reminder message: ${result['message']}'); + return null; + } + + final rateReminde = NStackRateReminder.fromJson(result['data']); + return rateReminde; + } catch (e) { + // Log rest of the rate reminder errors + LogUtil.log(e); + return null; + } + } + + Future postRateReminderEvent({ + required NStackAppInfo appInfoData, + required String action, + }) async { + final url = Uri.parse('$_baseUrl/notify/rate_reminder_v2/events'); + + final requestBody = { + 'guid': appInfoData.guid, + 'action': action, + }; + + final response = await http.post( + url, + headers: _headers, + body: requestBody, + ); + + if (response.statusCode != 201) { + throw NStackException.updateFailed( + 'Failed to update rate reminder action.', + ); + } + } + + Future postRateReminderAnswer({ + required NStackAppInfo appInfoData, + required String answer, + required int rateReminderId, + }) async { + final url = + Uri.parse('$_baseUrl/notify/rate_reminder_v2/$rateReminderId/answers'); + + final requestBody = { + 'guid': appInfoData.guid, + 'answer': answer, + }; + + final response = await http.post( + url, + headers: _headers, + body: requestBody, + ); + + if (response.statusCode != 201) { + throw NStackException.updateFailed( + 'Failed to update rate reminder answer.', + ); + } + } } diff --git a/lib/src/sdk/nstack_features.dart b/lib/src/sdk/nstack_features.dart index 217aa4b..9a33191 100644 --- a/lib/src/sdk/nstack_features.dart +++ b/lib/src/sdk/nstack_features.dart @@ -1,29 +1,56 @@ import 'package:nstack/src/models/nstack_message.dart'; +import 'package:nstack/src/models/nstack_rate_reminder_answer.dart'; import 'package:nstack/src/models/nstack_version_update.dart'; +import 'package:nstack/src/models/rate_reminder.dart'; sealed class NStackFeatureHandler {} -typedef OnMessage = void Function(NStackMessage? message)?; -typedef OnVersionUpdateNotification = void Function( +typedef MessageCallback = void Function(NStackMessage? message); +typedef VersionUpdateCallback = void Function( NStackVersionUpdate? updateInfo, -)?; +); +typedef RateReminderCallback = void Function( + NStackRateReminder rateReminderInfo, +); +typedef RateReminderAnswereCallback = void Function( + NStackRateReminderAnswer rateReminderAnswer, +); final class NStackMessageHandler implements NStackFeatureHandler { NStackMessageHandler._({this.onMessage}); - factory NStackMessageHandler.config({OnMessage? onMessage}) => + factory NStackMessageHandler.config({MessageCallback? onMessage}) => NStackMessageHandler._(onMessage: onMessage); - final OnMessage? onMessage; + final MessageCallback? onMessage; } final class NStackVersionUpdateHandler implements NStackFeatureHandler { NStackVersionUpdateHandler._({this.onVersionUpdateNotification}); factory NStackVersionUpdateHandler.config({ - OnVersionUpdateNotification? onVersionUpdateNotification, + VersionUpdateCallback? onVersionUpdateNotification, }) => NStackVersionUpdateHandler._( onVersionUpdateNotification: onVersionUpdateNotification, ); - final OnVersionUpdateNotification? onVersionUpdateNotification; + final VersionUpdateCallback? onVersionUpdateNotification; +} + +final class NStackRateReminderHandler implements NStackFeatureHandler { + NStackRateReminderHandler._({ + this.onRateReminder, + this.onRateReminderAnswered, + }); + + factory NStackRateReminderHandler.config({ + RateReminderCallback? onRateReminder, + RateReminderAnswereCallback? onRateReminderAnswered, + }) => + NStackRateReminderHandler._( + onRateReminder: onRateReminder, + onRateReminderAnswered: onRateReminderAnswered, + ); + + final RateReminderCallback? onRateReminder; + final RateReminderAnswereCallback? onRateReminderAnswered; } diff --git a/lib/src/sdk/nstack_sdk.dart b/lib/src/sdk/nstack_sdk.dart index 9510ae9..35fb29d 100644 --- a/lib/src/sdk/nstack_sdk.dart +++ b/lib/src/sdk/nstack_sdk.dart @@ -10,6 +10,7 @@ import 'package:nstack/src/repository/nstack_localization_repository.dart'; import 'package:nstack/src/repository/nstack_repository.dart'; import 'package:nstack/src/sdk/localization/nstack_localization.dart'; import 'package:nstack/src/sdk/messages/nstack_messages.dart'; +import 'package:nstack/src/sdk/rate_reminders/nstack_rate_reminders.dart'; import 'package:nstack/src/sdk/version_control/nstack_version_control.dart'; import 'package:nstack/src/utils/log_util.dart'; import 'package:package_info/package_info.dart'; @@ -40,6 +41,7 @@ class NStackSdk { final NStackLocalization localization; late final NStackMessages messages; late final NStackVersionControl appVersionControl; + late final NStackRateReminders rateReminders; var _appOpenCalled = false; @@ -57,6 +59,13 @@ class NStackSdk { ); final localizationInit = await localization.init(); + + rateReminders = NStackRateReminders( + repository: _repository, + appInfoData: _appInfoData, + localization: localization, + ); + return localizationInit; } diff --git a/lib/src/sdk/rate_reminders/nstack_rate_reminders.dart b/lib/src/sdk/rate_reminders/nstack_rate_reminders.dart new file mode 100644 index 0000000..6baacd0 --- /dev/null +++ b/lib/src/sdk/rate_reminders/nstack_rate_reminders.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:nstack/src/models/nstack_app_info.dart'; +import 'package:nstack/src/models/nstack_rate_reminder_answer.dart'; +import 'package:nstack/src/models/rate_reminder.dart'; +import 'package:nstack/src/repository/nstack_repository.dart'; +import 'package:nstack/src/sdk/localization/nstack_localization.dart'; +import 'package:nstack/src/utils/log_util.dart'; + +class NStackRateReminders { + NStackRateReminders({ + required NStackRepository repository, + required NStackAppInfo appInfoData, + required NStackLocalization localization, + }) : _repository = repository, + _appInfoData = appInfoData, + _localization = localization; + + final NStackRepository _repository; + final NStackAppInfo _appInfoData; + final NStackLocalization _localization; + + Future getRateReminderInfo({ + required Locale defaultLocale, + }) async { + final selectedLanguageTag = + await _localization.getUserSelectedLanguageTag() ?? + defaultLocale.toLanguageTag(); + + final result = _repository.getRateReminderInfo( + acceptHeader: selectedLanguageTag, + appInfoData: _appInfoData, + ); + + return result; + } + + Future postRateReminderEvent({ + required String action, + }) async { + try { + await _repository.postRateReminderEvent( + appInfoData: _appInfoData, + action: action, + ); + } catch (e) { + LogUtil.log('Could not post rate reminder event.'); + } + } + + Future postRateReminderAnswer({ + required NStackRateReminderAnswer answer, + required int rateReminderId, + }) async { + try { + await _repository.postRateReminderAnswer( + appInfoData: _appInfoData, + answer: answer.name, + rateReminderId: rateReminderId, + ); + } catch (e) { + LogUtil.log('Could not post rate reminder answer.'); + } + } +} diff --git a/lib/src/sdk/widgets/nstack_feature_handler_widget.dart b/lib/src/sdk/widgets/nstack_feature_handler_widget.dart index e31a6b2..540c1a6 100644 --- a/lib/src/sdk/widgets/nstack_feature_handler_widget.dart +++ b/lib/src/sdk/widgets/nstack_feature_handler_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:nstack/src/sdk/nstack_features.dart'; import 'package:nstack/src/sdk/widgets/nstack_base_widget.dart'; import 'package:nstack/src/sdk/widgets/nstack_message_widget.dart'; +import 'package:nstack/src/sdk/widgets/nstack_rate_reminder_widget.dart'; import 'package:nstack/src/sdk/widgets/nstack_version_control_widget.dart'; class NStackFeatureHandlerWidget extends StatefulWidget { @@ -43,6 +44,12 @@ class _NStackFeatureHandlerState extends State { onVersionUpdateNotification: feature.onVersionUpdateNotification, child: child, ); + case NStackRateReminderHandler(): + child = NStackRateReminderWidget( + onRateReminder: feature.onRateReminder, + onAnswered: feature.onRateReminderAnswered, + child: child, + ); } } return child; diff --git a/lib/src/sdk/widgets/nstack_message_widget.dart b/lib/src/sdk/widgets/nstack_message_widget.dart index dca65b4..70596a3 100644 --- a/lib/src/sdk/widgets/nstack_message_widget.dart +++ b/lib/src/sdk/widgets/nstack_message_widget.dart @@ -9,6 +9,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:nstack/src/models/nstack_message.dart'; import 'package:nstack/src/sdk/extensions/nstack_widget_extension.dart'; +import 'package:nstack/src/sdk/nstack_features.dart'; import 'package:nstack/src/sdk/widgets/adaptive_dialog_action_widget.dart'; import 'package:nstack/src/utils/log_util.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -16,20 +17,18 @@ import 'package:url_launcher/url_launcher.dart'; class NStackMessageWidget extends StatefulWidget { const NStackMessageWidget({ super.key, - this.child, this.onMessage, + this.child, }); + final MessageCallback? onMessage; final Widget? child; - final void Function(NStackMessage?)? onMessage; @override State createState() => _NStackMessageWidgetSate(); } class _NStackMessageWidgetSate extends State { - StreamSubscription? _messageSubscription; - @override void didChangeDependencies() { super.didChangeDependencies(); @@ -42,12 +41,6 @@ class _NStackMessageWidgetSate extends State { ); } - @override - void dispose() { - _messageSubscription?.cancel(); - super.dispose(); - } - void _onMessage(NStackMessage? message) { if (widget.onMessage != null) { widget.onMessage!(message); diff --git a/lib/src/sdk/widgets/nstack_rate_reminder_widget.dart b/lib/src/sdk/widgets/nstack_rate_reminder_widget.dart new file mode 100644 index 0000000..82626b7 --- /dev/null +++ b/lib/src/sdk/widgets/nstack_rate_reminder_widget.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:nstack/src/models/nstack_rate_reminder_answer.dart'; +import 'package:nstack/src/models/rate_reminder.dart'; +import 'package:nstack/src/sdk/extensions/nstack_widget_extension.dart'; +import 'package:nstack/src/sdk/nstack_features.dart'; +import 'package:nstack/src/sdk/widgets/adaptive_dialog_action_widget.dart'; +import 'package:nstack/src/utils/log_util.dart'; + +class NStackRateReminderWidget extends StatefulWidget { + const NStackRateReminderWidget({ + super.key, + this.onRateReminder, + this.onAnswered, + this.child, + }) : assert( + onRateReminder != null || onAnswered != null, + 'Either onRateReminder or onAnswered must be provided.', + ); + + final RateReminderCallback? onRateReminder; + final RateReminderAnswereCallback? onAnswered; + final Widget? child; + + @override + State createState() => _NStackRateReminderWidgetState(); +} + +class _NStackRateReminderWidgetState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) async { + final rateReminderInfo = + await context.nstack.rateReminders.getRateReminderInfo( + defaultLocale: Localizations.localeOf(context), + ); + if (rateReminderInfo != null) { + _onRateReminder(rateReminderInfo); + } + }, + ); + } + + void _onRateReminder(NStackRateReminder rateReminder) { + if (widget.onRateReminder != null) { + widget.onRateReminder!(rateReminder); + return; + } + if (widget.onAnswered != null) { + _NStackRateReminderDialog.show( + context, + rateReminder: rateReminder, + onAnswered: widget.onAnswered!, + ); + } + } + + @override + Widget build(BuildContext context) { + return widget.child ?? const SizedBox(); + } +} + +class _NStackRateReminderDialog extends StatelessWidget { + const _NStackRateReminderDialog._({ + Key? key, + required this.rateReminder, + required this.onAnswered, + }) : super(key: key); + + /// Fallback titles + final _titleFallback = 'Enjoying the App?'; + final _bodyFallback = + 'We hope you are loving our app! If you have a moment, please rate us.'; + final _yesButtonTitleFallback = 'Yes'; + final _noButtonTitleFallback = 'No'; + final _laterButtonTitleFallback = 'Later'; + + /// Rate Reminder info that was received. + final NStackRateReminder rateReminder; + + /// Callback for the answer + final void Function(NStackRateReminderAnswer)? onAnswered; + + /// Displays the dialog. + static Future show( + BuildContext context, { + required NStackRateReminder rateReminder, + required Function(NStackRateReminderAnswer) onAnswered, + }) { + Widget builder(BuildContext context) { + return _NStackRateReminderDialog._( + rateReminder: rateReminder, + onAnswered: onAnswered, + ); + } + + return showAdaptiveDialog( + context: context, + builder: builder, + ); + } + + @override + Widget build(BuildContext context) { + final messageTitleWidget = + Text(rateReminder.localization?.title ?? _titleFallback); + + final messageBodyWidget = + Text(rateReminder.localization?.body ?? _bodyFallback); + + final yesButtonTitleWidget = Text( + rateReminder.localization?.yesBtn ?? _yesButtonTitleFallback, + ); + final noButtonTitleWidget = Text( + rateReminder.localization?.noBtn ?? _noButtonTitleFallback, + ); + + final laterButtonTitleWidget = Text( + rateReminder.localization?.laterBtn ?? _laterButtonTitleFallback, + ); + + return AlertDialog.adaptive( + title: messageTitleWidget, + content: messageBodyWidget, + actions: [ + AdaptiveDialogAction( + onPressed: () async { + await _buttonAction( + context, + NStackRateReminderAnswer.positive, + rateReminder.id, + ); + }, + child: yesButtonTitleWidget, + ), + AdaptiveDialogAction( + onPressed: () async { + await _buttonAction( + context, + NStackRateReminderAnswer.negative, + rateReminder.id, + ); + }, + child: noButtonTitleWidget, + ), + AdaptiveDialogAction( + onPressed: () async { + await _buttonAction( + context, + NStackRateReminderAnswer.skip, + rateReminder.id, + ); + }, + child: laterButtonTitleWidget, + ), + ], + ); + } + + Future _buttonAction( + BuildContext context, + NStackRateReminderAnswer answer, + int rateReminderId, + ) async { + try { + await context.nstack.rateReminders.postRateReminderAnswer( + answer: answer, + rateReminderId: rateReminderId, + ); + if (onAnswered != null) { + onAnswered!(answer); + } + } catch (e) { + LogUtil.log( + 'Filed to post answer', + 'NStackRateReminder', + ); + } + + if (context.mounted) { + Navigator.of(context).pop(); + } + } +} diff --git a/lib/src/sdk/widgets/nstack_version_control_widget.dart b/lib/src/sdk/widgets/nstack_version_control_widget.dart index 7b80e94..2985de0 100644 --- a/lib/src/sdk/widgets/nstack_version_control_widget.dart +++ b/lib/src/sdk/widgets/nstack_version_control_widget.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:nstack/src/models/nstack_version_update.dart'; import 'package:nstack/src/models/nstack_version_update_view_request.dart'; import 'package:nstack/src/sdk/extensions/nstack_widget_extension.dart'; +import 'package:nstack/src/sdk/nstack_features.dart'; import 'package:nstack/src/sdk/widgets/adaptive_dialog_action_widget.dart'; import 'package:nstack/src/utils/log_util.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -17,12 +18,12 @@ import 'package:url_launcher/url_launcher.dart'; class NStackVersionControlWidget extends StatefulWidget { const NStackVersionControlWidget({ super.key, - this.child, this.onVersionUpdateNotification, + this.child, }); + final VersionUpdateCallback? onVersionUpdateNotification; final Widget? child; - final void Function(NStackVersionUpdate?)? onVersionUpdateNotification; @override State createState() => _NStackVersionControlWidgetSate(); @@ -30,8 +31,6 @@ class NStackVersionControlWidget extends StatefulWidget { class _NStackVersionControlWidgetSate extends State { - StreamSubscription? _versionInfoSubscription; - @override void didChangeDependencies() { super.didChangeDependencies(); @@ -44,12 +43,6 @@ class _NStackVersionControlWidgetSate ); } - @override - void dispose() { - _versionInfoSubscription?.cancel(); - super.dispose(); - } - void _onVersionUpdateNotification(NStackVersionUpdate? updateInfo) { if (widget.onVersionUpdateNotification != null) { widget.onVersionUpdateNotification!(updateInfo); diff --git a/lib/templates/nstack_template.txt b/lib/templates/nstack_template.txt index 8d5a5bf..e5f1beb 100644 --- a/lib/templates/nstack_template.txt +++ b/lib/templates/nstack_template.txt @@ -127,6 +127,55 @@ * `NStackFeatureHandlerWidget` with the `NStackVersionUpdateHandler` config, * and you will get the version update data only once for an app life cycle. * + * ⭐ Rate Reminder + * + * Rate reminders ask the user to rate the app on the platform-specific store once the amount of points from events reaches a certain threshold. + * Use `NStackFeatureHandlerWidget` with `NStackRateReminderHandler` config to integrate the rate reminder feature within your Flutter app. + * And use `postRateReminderEvent`API from `NStackRateReminders` to post points to the backend. + * This setup facilitates different strategies for handling the Rate Reminder + * + * - Default Adaptive Dialog: + * By default, `NStackRateReminderHandler.config()` with `void Function(NStackRateReminderAnswer)` callback is designed to automatically handle rate reminder. + * The callback will provide the user's answer. + * Example: + * + * ```dart + * NStackRateReminderHandler.config( + * onRateReminderAnswered: (rateReminderAnswer) { + * + * }, + * ) + * ``` + * + * - Custom Handling: + * By passing a `void Function(NStackRateReminder)` callback to `NStackRateReminderHandler.config()`, + * you gain control over how the rate reminder alert is presented to the user. In this case, passing the + * `void Function(NStackRateReminderAnswer)` callback is unnecessary. + * Example: + * + * ```dart + * NStackRateReminderHandler.config( + * onRateReminder: (rateReminderInfo) { + * + * }, + * ) + * ``` + * + * Or, if you don't want to use the `NStackFeatureHandlerWidget`, + * you can use the `getRateReminderInfo`, `postRateReminderEvent` and `postRateReminderAnswer` from `NStackRateReminders`. + * Example: + * + * ```dart + * WidgetsBinding.instance.addPostFrameCallback( + * (timeStamp) async { + * final rateReminderInfo = + * await context.nstack.rateReminders.getRateReminderInfo( + * defaultLocale: Localizations.localeOf(context), + * ); + * }, + * ); + * ``` + * * 🛠️ IMPORTANT NOTES FOR SDK USERS * * The default environment for the NStack SDK is `prod`. @@ -151,6 +200,7 @@ import 'package:nstack/src/sdk/localization/section_key_delegate.dart'; import 'package:nstack/src/sdk/widgets/nstack_base_widget.dart'; export 'package:nstack/src/models/app_open_platform.dart'; +export 'package:nstack/src/models/nstack_rate_reminder_answer.dart'; export 'package:nstack/src/models/nstack_version_update_view_request.dart' show NStackVersionUpdateViewAnswer, NStackVersionUpdateViewType; export 'package:nstack/src/sdk/extensions/nstack_widget_extension.dart'; @@ -199,18 +249,20 @@ class NStackLocalizationAsset { * */ - class NStackWidget extends NStackBaseWidget { +class NStackWidget extends NStackBaseWidget { NStackWidget({ Key? key, required Widget child, AppOpenPlatform? platformOverride, VoidCallback? onComplete, + bool testMode = false, }) : super( key: key, child: child, platformOverride: platformOverride, onComplete: onComplete, config: config, + testMode: testMode, localization: NStackLocalization( config: config, bundledLocalization: BundledLocalizationImpl.data(),