From 992202d637af30109d6f82276db26114c5f86861 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 13:40:45 +0300 Subject: [PATCH 01/13] refactor(#51): Depend on repo in NStack classes --- lib/sdk/localization/nstack_localization.dart | 8 +++----- lib/sdk/nstack_sdk.dart | 10 +++++----- lib/src/nstack_builder.dart | 10 +++++++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/sdk/localization/nstack_localization.dart b/lib/sdk/localization/nstack_localization.dart index 65f58dd..3da668c 100644 --- a/lib/sdk/localization/nstack_localization.dart +++ b/lib/sdk/localization/nstack_localization.dart @@ -6,7 +6,6 @@ import 'package:nstack/models/app_open.dart'; import 'package:nstack/models/language.dart'; import 'package:nstack/models/language_response.dart'; import 'package:nstack/models/localize_index.dart'; -import 'package:nstack/models/nstack_config.dart'; import 'package:nstack/src/nstack_repository.dart'; import 'package:nstack/src/repository.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,9 +17,8 @@ class NStackLocalization { final _onLocaleChanged = StreamController.broadcast(); - final NStackConfig config; - final TLocalization translations; final NStackRepository _repository; + final TLocalization translations; late List supportedLocales; final bool isDebug; @@ -39,13 +37,13 @@ class NStackLocalization { Locale? clientLocale; NStackLocalization({ - required this.config, + required NStackRepository repository, required this.translations, required List availableLanguages, required Map bundledTranslations, required String pickedLanguageLocale, required this.isDebug, - }) : _repository = NStackRepository(config) { + }) : _repository = repository { supportedLocales = availableLanguages .map((e) => Locale(e.language?.locale?.split("-")[0] ?? "en", e.language?.locale?.split("-")[1].toUpperCase() ?? "US")) diff --git a/lib/sdk/nstack_sdk.dart b/lib/sdk/nstack_sdk.dart index 4b2a285..4b7ef13 100644 --- a/lib/sdk/nstack_sdk.dart +++ b/lib/sdk/nstack_sdk.dart @@ -5,8 +5,8 @@ import 'package:flutter/widgets.dart'; import 'package:nstack/models/app_open.dart'; import 'package:nstack/models/app_open_platform.dart'; import 'package:nstack/models/nstack_appopen_data.dart'; -import 'package:nstack/models/nstack_config.dart'; import 'package:nstack/sdk/localization/nstack_localization.dart'; +import 'package:nstack/sdk/messages/nstack_messages.dart'; import 'package:nstack/src/nstack_repository.dart'; import 'package:nstack/src/repository.dart'; import 'package:package_info/package_info.dart'; @@ -15,8 +15,6 @@ import 'package:uuid/uuid.dart'; /// The core class containing all the NStack features. class NStackSdk { - final NStackConfig config; - final String _prefsKeyLastUpdated = "nstack_last_updated"; final String _prefsKeyGuid = "nstack_guid"; @@ -25,14 +23,16 @@ class NStackSdk { final bool isDebug; final NStackLocalization localization; + final NStackMessages messages; var _appOpenCalled = false; NStackSdk({ - required this.config, + required NStackRepository repository, required this.isDebug, required this.localization, - }) : _repository = NStackRepository(config); + required this.messages, + }) : _repository = repository; Future _setupAppOpenData(AppOpenPlatform? platformOverride) async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/src/nstack_builder.dart b/lib/src/nstack_builder.dart index ceda1ff..a650e5c 100644 --- a/lib/src/nstack_builder.dart +++ b/lib/src/nstack_builder.dart @@ -124,6 +124,7 @@ import 'package:nstack/models/language.dart'; import 'package:nstack/models/localize_index.dart'; import 'package:nstack/models/nstack_config.dart'; import 'package:nstack/sdk/nstack_sdk.dart'; +import 'package:nstack/src/nstack_repository.dart'; import 'package:nstack/sdk/localization/nstack_localization.dart'; import 'package:nstack/partial/section_key_delegate.dart'; @@ -142,13 +143,16 @@ export 'package:nstack/models/app_open_platform.dart'; */ final NStack = NStackSdk( - config: _config, - localization: _nstackLocalization, + repository: _nstackRepository, isDebug: kDebugMode, + localization: _nstackLocalization, + messages: _nstackMessages, ); +const _nstackRepository = NStackRepository(_config); + final _nstackLocalization = NStackLocalization( - config: _config, + repository: _nstackRepository, translations: const Localization(), availableLanguages: _languages, bundledTranslations: _bundledTranslations, From 3bc2288b0eec32a3b10057b2980e88e86d9361fe Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 13:41:15 +0300 Subject: [PATCH 02/13] chore(#51): Start introducing messages --- lib/sdk/messages/nstack_messages.dart | 13 +++++++++++++ lib/src/nstack_builder.dart | 5 +++++ 2 files changed, 18 insertions(+) create mode 100644 lib/sdk/messages/nstack_messages.dart diff --git a/lib/sdk/messages/nstack_messages.dart b/lib/sdk/messages/nstack_messages.dart new file mode 100644 index 0000000..7b24905 --- /dev/null +++ b/lib/sdk/messages/nstack_messages.dart @@ -0,0 +1,13 @@ +import 'package:nstack/src/nstack_repository.dart'; + +class NStackMessages { + final NStackRepository _repository; + + NStackMessages({ + required NStackRepository repository, + }) : _repository = repository; + + void setMessageViewed(int messageId) { + // TODO: Set message viewed + } +} diff --git a/lib/src/nstack_builder.dart b/lib/src/nstack_builder.dart index a650e5c..51ab8f3 100644 --- a/lib/src/nstack_builder.dart +++ b/lib/src/nstack_builder.dart @@ -126,6 +126,7 @@ import 'package:nstack/models/nstack_config.dart'; import 'package:nstack/sdk/nstack_sdk.dart'; import 'package:nstack/src/nstack_repository.dart'; import 'package:nstack/sdk/localization/nstack_localization.dart'; +import 'package:nstack/sdk/messages/nstack_messages.dart'; import 'package:nstack/partial/section_key_delegate.dart'; export 'package:nstack/models/app_open_platform.dart'; @@ -159,6 +160,10 @@ final _nstackLocalization = NStackLocalization( pickedLanguageLocale: '', isDebug: kDebugMode, ); + +final _nstackMessages = NStackMessages( + repository: _nstackRepository, +); '''); } From be580f75555c2a85f83541a5ad762aa7bcad041a Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 17:37:25 +0300 Subject: [PATCH 03/13] fix(#51): Message: make certain props non nullable --- lib/models/message.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/message.dart b/lib/models/message.dart index 303145d..ee6ea42 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -1,11 +1,11 @@ import 'package:nstack/other/extensions.dart'; class Message { - final int? id; - final int? applicationId; + final int id; + final int applicationId; final MessageShowSetting? showSetting; final int? viewCount; - final String? message; + final String message; final String? url; final DateTime? createdAt; final DateTime? updatedAt; From 10b446b361d23519504ff0da2f95b6711f7f9a05 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 17:37:45 +0300 Subject: [PATCH 04/13] chore(#51): Add URL launcher package --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 863dd53..34c1bb2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: uuid: ^3.0.4 dart_style: ^2.0.2 collection: ^1.15.0 + url_launcher: ^6.0.10 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 8202ca61b9f5d924a8cc638ec0679ecaa4680dc7 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 17:38:37 +0300 Subject: [PATCH 05/13] feat(#51): Call onAppOpen on messages + stream it --- lib/sdk/messages/nstack_messages.dart | 20 +++++++++++++++++++- lib/sdk/nstack_sdk.dart | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/sdk/messages/nstack_messages.dart b/lib/sdk/messages/nstack_messages.dart index 7b24905..bca998d 100644 --- a/lib/sdk/messages/nstack_messages.dart +++ b/lib/sdk/messages/nstack_messages.dart @@ -1,13 +1,31 @@ +import 'dart:async'; + +import 'package:nstack/models/app_open.dart'; +import 'package:nstack/models/message.dart'; import 'package:nstack/src/nstack_repository.dart'; class NStackMessages { final NStackRepository _repository; + // There's no dispose / close method + // ignore: close_sinks + final _onMessage = StreamController.broadcast(); + + Stream get onMessage => _onMessage.stream; + NStackMessages({ required NStackRepository repository, }) : _repository = repository; - void setMessageViewed(int messageId) { + Future setMessageViewed(int messageId) async { // TODO: Set message viewed } + + void onAppOpen(AppOpen appOpen) { + final message = appOpen.data.message; + + if (message != null) { + _onMessage.add(message); + } + } } diff --git a/lib/sdk/nstack_sdk.dart b/lib/sdk/nstack_sdk.dart index 4b7ef13..9476ee0 100644 --- a/lib/sdk/nstack_sdk.dart +++ b/lib/sdk/nstack_sdk.dart @@ -119,6 +119,8 @@ class NStackSdk { await localization.updateOnAppOpen(appOpen); _log('NStack --> Updated localization.'); + messages.onAppOpen(appOpen); + _appOpenCalled = true; return AppOpenResult.success; From 7951cccdd1ef9ae7af2b7cfbef777f7a75785c54 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 17:42:30 +0300 Subject: [PATCH 06/13] feat(#51): Generate NStack Messages widgets --- lib/nstack.dart | 2 + lib/src/nstack_builder.dart | 188 +++++++++++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 lib/nstack.dart diff --git a/lib/nstack.dart b/lib/nstack.dart new file mode 100644 index 0000000..40c6a23 --- /dev/null +++ b/lib/nstack.dart @@ -0,0 +1,2 @@ +export 'package:nstack/sdk/nstack_sdk.dart'; +export 'package:url_launcher/url_launcher.dart'; diff --git a/lib/src/nstack_builder.dart b/lib/src/nstack_builder.dart index 51ab8f3..bf061c7 100644 --- a/lib/src/nstack_builder.dart +++ b/lib/src/nstack_builder.dart @@ -92,6 +92,8 @@ class NstackBuilder implements Builder { void _writeHeader(StringBuffer output) { output.writeln( ''' +// ignore_for_file: implementation_imports, non_constant_identifier_names + /* * ❌ GENERATED BY NSTACK, DO NOT MODIFY THIS FILE BY HAND! * @@ -116,18 +118,23 @@ class NstackBuilder implements Builder { */ import 'dart:async'; +import 'dart:io'; +import 'package:flutter/cupertino.dart' as cupertino; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' as material; import 'package:flutter/widgets.dart'; import 'package:nstack/models/app_open_platform.dart'; import 'package:nstack/models/language.dart'; import 'package:nstack/models/localize_index.dart'; +import 'package:nstack/models/message.dart'; import 'package:nstack/models/nstack_config.dart'; -import 'package:nstack/sdk/nstack_sdk.dart'; import 'package:nstack/src/nstack_repository.dart'; import 'package:nstack/sdk/localization/nstack_localization.dart'; import 'package:nstack/sdk/messages/nstack_messages.dart'; import 'package:nstack/partial/section_key_delegate.dart'; +import 'package:nstack/nstack.dart'; + export 'package:nstack/models/app_open_platform.dart'; ''', @@ -322,9 +329,9 @@ class NStackScope extends InheritedWidget { } class NStackWidget extends StatefulWidget { - final Widget child; final AppOpenPlatform? platformOverride; final VoidCallback? onComplete; + final Widget child; const NStackWidget({Key? key, required this.child, this.platformOverride, this.onComplete,}) : super(key: key); @@ -341,6 +348,9 @@ class NStackState extends State { late final StreamSubscription _localeChangedSubscription; + NStackLocalization get localization => _nstack.localization; + NStackMessages get messages => _nstack.messages; + @override void initState() { super.initState(); @@ -392,6 +402,180 @@ class NStackState extends State { } } +class NStackMessageListener extends StatefulWidget { + const NStackMessageListener({ + Key? key, + required this.onMessage, + this.child, + }) : super(key: key); + + final void Function(Message message) onMessage; + final Widget? child; + + @override + State createState() => _NStackMessageListenerState(); +} + +class _NStackMessageListenerState extends State { + late final StreamSubscription _subscription; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final scope = NStackScope.of(context); + _subscription = scope.messages.onMessage.listen(widget.onMessage); + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child ?? const SizedBox(); + } +} + +typedef MessageDialogBuilder = Widget Function( + BuildContext context, + Message message, +); + +class NStackMessageDialog extends StatelessWidget { + /// Default Message Alert Dialog. + const NStackMessageDialog({ + Key? key, + required this.message, + this.onOkPressed, + this.onOpenUrlPressed, + this.okButtonTitle = 'OK', + this.openUrlButtonTitle = 'Open URL', + this.dialogTitle = 'Message', + }) : _customBuilder = null, + super(key: key); + + /// Using this constructor, you can define your own Alert Dialog layout + /// by specifying the [builder] parameter. + const NStackMessageDialog.customBuilder({ + Key? key, + required this.message, + + /// Custom alert dialog builder. + required MessageDialogBuilder builder, + }) : onOkPressed = null, + onOpenUrlPressed = null, + okButtonTitle = '', + openUrlButtonTitle = '', + dialogTitle = '', + _customBuilder = builder, + super(key: key); + + /// Message that was received. + final Message message; + + /// Title of the OK button. + final String okButtonTitle; + + /// Title of the Open URL button. + final String openUrlButtonTitle; + + /// Title of the dialog. + final String dialogTitle; + + /// Optional callback when a user presses the OK button. + /// + /// By default, it closes the dialog and reports that the message was viewed. + final VoidCallback? onOkPressed; + + /// Optional callback when a user presses Open URL button. + /// + /// By default, it closes the dialog, reports that the message was viewed + /// and opens the URL. + final void Function(Uri uri)? onOpenUrlPressed; + + final MessageDialogBuilder? _customBuilder; + + @override + Widget build(BuildContext context) { + final customBuilder = _customBuilder; + + if (customBuilder != null) { + return customBuilder(context, message); + } + + final titleWidget = Text(dialogTitle); + final messageWidget = Text(message.message); + + final okWidget = Text(okButtonTitle); + final okAction = onOkPressed ?? + () { + Navigator.of(context).pop(); + NStackScope.of(context).messages.setMessageViewed(message.id); + }; + + final messageUrl = message.url; + final Uri? uri; + + if (messageUrl != null) { + uri = Uri.tryParse(messageUrl); + } else { + uri = null; + } + + final isUriValid = uri != null; + + final urlLaunchAction = !isUriValid + ? null + : (onOpenUrlPressed != null ? () => onOpenUrlPressed!(uri!) : null) ?? + () { + launchUrl(uri!); + + Navigator.of(context).pop(); + NStackScope.of(context).messages.setMessageViewed(message.id); + }; + + if (Platform.isIOS) { + return cupertino.CupertinoAlertDialog( + title: titleWidget, + content: messageWidget, + actions: [ + cupertino.CupertinoDialogAction( + onPressed: okAction, + child: okWidget, + ), + if (isUriValid) + cupertino.CupertinoDialogAction( + isDefaultAction: true, + onPressed: urlLaunchAction, + child: Text(openUrlButtonTitle), + ), + ], + ); + } + + return material.AlertDialog( + title: titleWidget, + content: messageWidget, + actions: [ + if (isUriValid) + material.TextButton( + onPressed: urlLaunchAction, + child: Text(openUrlButtonTitle), + ), + material.TextButton( + onPressed: okAction, + child: okWidget, + ), + ], + ); + } +} + /* * * NStack Flutter Extensions From a0a397913cdf625489fc6ee7530bc52f5b934adc Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 18:00:03 +0300 Subject: [PATCH 07/13] refactor(#51): Refactor the buidler + provide docs --- lib/src/nstack_builder.dart | 121 ++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 34 deletions(-) diff --git a/lib/src/nstack_builder.dart b/lib/src/nstack_builder.dart index bf061c7..20d4ed9 100644 --- a/lib/src/nstack_builder.dart +++ b/lib/src/nstack_builder.dart @@ -313,6 +313,7 @@ const _bundledTranslations = {'''); * NStack Flutter Widgets * */ +/// Allows to access NStack features via a `BuildContext`. class NStackScope extends InheritedWidget { final NStackState state; final String checksum; @@ -328,6 +329,23 @@ class NStackScope extends InheritedWidget { checksum != oldWidget.checksum; } +/// Widget that is used for accessing NStack features from the widget tree +/// & listening for localization changes. +/// +/// Is required for all the children widgets like [NStackMessageListener] +/// +/// In your app, use the `builder` property like this: +/// ```dart +/// MaterialApp( +/// ... +/// builder: (context, child) { +/// return NStackWidget( +/// child: child!, +/// ); +/// }, +/// ... +/// ); +/// ``` class NStackWidget extends StatefulWidget { final AppOpenPlatform? platformOverride; final VoidCallback? onComplete; @@ -348,7 +366,10 @@ class NStackState extends State { late final StreamSubscription _localeChangedSubscription; + /// Gets the NStack Localization feature configured for this project. NStackLocalization get localization => _nstack.localization; + + /// Gets the NStack Message feature configured for this project. NStackMessages get messages => _nstack.messages; @override @@ -402,6 +423,25 @@ class NStackState extends State { } } +/// Listens for new messages from the NStack Messages feature. +/// +/// In where you want to use it, add this widget: +/// ```dart +/// Widget build(BuildContext context) { +/// return NStackMessageListener( +/// onMessage: (Message message) { +/// // Do whatever you want with the received message. +/// // For example, use NStackMessageDialog to display the message. +/// showDialog( +/// context: context, +/// builder: (context) { +/// return NStackMessageDialog(message: message); +/// }, +/// }, +/// child: Scaffold(...), +/// ); +/// } +/// ``` class NStackMessageListener extends StatefulWidget { const NStackMessageListener({ Key? key, @@ -446,9 +486,23 @@ typedef MessageDialogBuilder = Widget Function( Message message, ); + /// Default Message Alert Dialog that adapts to the platform and renders the: + /// - Message body + /// - OK Button + /// - Open URL button (if [Message.url] is provided) + /// + /// When the dialog is dismissed, the alert reports that the message is viewed. + /// + /// Use it like this: + /// ```dart + /// NStackMessageDialog.show( + /// context, + /// message: message, + /// /* Other params if needed */ + /// ); + /// ``` class NStackMessageDialog extends StatelessWidget { - /// Default Message Alert Dialog. - const NStackMessageDialog({ + const NStackMessageDialog._({ Key? key, required this.message, this.onOkPressed, @@ -456,24 +510,7 @@ class NStackMessageDialog extends StatelessWidget { this.okButtonTitle = 'OK', this.openUrlButtonTitle = 'Open URL', this.dialogTitle = 'Message', - }) : _customBuilder = null, - super(key: key); - - /// Using this constructor, you can define your own Alert Dialog layout - /// by specifying the [builder] parameter. - const NStackMessageDialog.customBuilder({ - Key? key, - required this.message, - - /// Custom alert dialog builder. - required MessageDialogBuilder builder, - }) : onOkPressed = null, - onOpenUrlPressed = null, - okButtonTitle = '', - openUrlButtonTitle = '', - dialogTitle = '', - _customBuilder = builder, - super(key: key); + }) : super(key: key); /// Message that was received. final Message message; @@ -498,25 +535,43 @@ class NStackMessageDialog extends StatelessWidget { /// and opens the URL. final void Function(Uri uri)? onOpenUrlPressed; - final MessageDialogBuilder? _customBuilder; + /// Displays the dialog. + static Future show( + BuildContext context, { + required Message message, + VoidCallback? onOkPressed, + void Function(Uri uri)? onOpenUrlPressed, + String okButtonTitle = 'OK', + String openUrlButtonTitle = 'Open URL', + String dialogTitle = 'Message', + }) { + builder(BuildContext context) { + return NStackMessageDialog._( + message: message, + onOkPressed: onOkPressed, + onOpenUrlPressed: onOpenUrlPressed, + okButtonTitle: okButtonTitle, + openUrlButtonTitle: openUrlButtonTitle, + dialogTitle: dialogTitle, + ); + } + + final showDialog = Platform.isIOS + ? cupertino.showCupertinoDialog(context: context, builder: builder) + : material.showDialog(context: context, builder: builder); + + return showDialog.whenComplete(() { + NStack.messages.setMessageViewed(message.id); + }); + } @override Widget build(BuildContext context) { - final customBuilder = _customBuilder; - - if (customBuilder != null) { - return customBuilder(context, message); - } - final titleWidget = Text(dialogTitle); final messageWidget = Text(message.message); final okWidget = Text(okButtonTitle); - final okAction = onOkPressed ?? - () { - Navigator.of(context).pop(); - NStackScope.of(context).messages.setMessageViewed(message.id); - }; + final okAction = onOkPressed ?? Navigator.of(context).pop; final messageUrl = message.url; final Uri? uri; @@ -534,9 +589,7 @@ class NStackMessageDialog extends StatelessWidget { : (onOpenUrlPressed != null ? () => onOpenUrlPressed!(uri!) : null) ?? () { launchUrl(uri!); - Navigator.of(context).pop(); - NStackScope.of(context).messages.setMessageViewed(message.id); }; if (Platform.isIOS) { From 87abe3834bead9a3a4ed1857c991a0040ebd948a Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 18:01:49 +0300 Subject: [PATCH 08/13] feat(#51): Update example --- example/lib/main.dart | 36 +++--- example/lib/nstack.dart | 255 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 270 insertions(+), 21 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5e98e97..35db08b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -14,7 +14,6 @@ class ExampleApp extends StatelessWidget { return MaterialApp( builder: (context, child) { return NStackWidget( - platformOverride: AppOpenPlatform.android, child: child!, ); }, @@ -30,21 +29,26 @@ class MainScreen extends StatelessWidget { Widget build(BuildContext context) { final activeLanguage = context.nstack.localization.activeLanguage; - return Scaffold( - appBar: AppBar( - title: Text(context.localization.test.testDollarSign), - ), - body: Center( - child: MaterialButton( - onPressed: () { - final locale = activeLanguage.locale == 'en-EN' - ? const Locale('de-DE') - : const Locale('en-EN'); - - NStack.localization.changeLocalization(locale); - }, - child: Text( - 'Selected locale: ${activeLanguage.name}', + return NStackMessageListener( + onMessage: (message) { + NStackMessageDialog.show(context, message: message); + }, + child: Scaffold( + appBar: AppBar( + title: Text(context.localization.test.testDollarSign), + ), + body: Center( + child: MaterialButton( + onPressed: () { + final locale = activeLanguage.locale == 'en-EN' + ? const Locale('de-DE') + : const Locale('en-EN'); + + NStack.localization.changeLocalization(locale); + }, + child: Text( + 'Selected locale: ${activeLanguage.name}', + ), ), ), ), diff --git a/example/lib/nstack.dart b/example/lib/nstack.dart index 52eb62a..93d8290 100644 --- a/example/lib/nstack.dart +++ b/example/lib/nstack.dart @@ -1,3 +1,5 @@ +// ignore_for_file: implementation_imports, non_constant_identifier_names + /* * ❌ GENERATED BY NSTACK, DO NOT MODIFY THIS FILE BY HAND! * @@ -22,16 +24,22 @@ */ import 'dart:async'; +import 'dart:io'; +import 'package:flutter/cupertino.dart' as cupertino; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' as material; import 'package:flutter/widgets.dart'; import 'package:nstack/models/app_open_platform.dart'; import 'package:nstack/models/language.dart'; import 'package:nstack/models/localize_index.dart'; +import 'package:nstack/models/message.dart'; import 'package:nstack/models/nstack_config.dart'; -import 'package:nstack/sdk/nstack_sdk.dart'; +import 'package:nstack/src/nstack_repository.dart'; import 'package:nstack/sdk/localization/nstack_localization.dart'; +import 'package:nstack/sdk/messages/nstack_messages.dart'; import 'package:nstack/partial/section_key_delegate.dart'; +import 'package:nstack/nstack.dart'; export 'package:nstack/models/app_open_platform.dart'; @@ -42,13 +50,16 @@ export 'package:nstack/models/app_open_platform.dart'; */ final NStack = NStackSdk( - config: _config, - localization: _nstackLocalization, + repository: _nstackRepository, isDebug: kDebugMode, + localization: _nstackLocalization, + messages: _nstackMessages, ); +const _nstackRepository = NStackRepository(_config); + final _nstackLocalization = NStackLocalization( - config: _config, + repository: _nstackRepository, translations: const Localization(), availableLanguages: _languages, bundledTranslations: _bundledTranslations, @@ -56,6 +67,10 @@ final _nstackLocalization = NStackLocalization( isDebug: kDebugMode, ); +final _nstackMessages = NStackMessages( + repository: _nstackRepository, +); + const _config = NStackConfig( projectId: 'h6wJremI2TGFM88gbLkdyljWQuwf2hxhxvCH', apiKey: 'zp2S18H32b67eYAbRQh94tVw76ZzaKKXlHjd', @@ -142,6 +157,7 @@ class _Test extends SectionKeyDelegate { * NStack Flutter Widgets * */ +/// Allows to access NStack features via a `BuildContext`. class NStackScope extends InheritedWidget { final NStackState state; final String checksum; @@ -161,10 +177,27 @@ class NStackScope extends InheritedWidget { checksum != oldWidget.checksum; } +/// Widget that is used for accessing NStack features from the widget tree +/// & listening for localization changes. +/// +/// Is required for all the children widgets like [NStackMessageListener] +/// +/// In your app, use the `builder` property like this: +/// ```dart +/// MaterialApp( +/// ... +/// builder: (context, child) { +/// return NStackWidget( +/// child: child!, +/// ); +/// }, +/// ... +/// ); +/// ``` class NStackWidget extends StatefulWidget { - final Widget child; final AppOpenPlatform? platformOverride; final VoidCallback? onComplete; + final Widget child; const NStackWidget({ Key? key, @@ -185,6 +218,12 @@ class NStackState extends State { late final StreamSubscription _localeChangedSubscription; + /// Gets the NStack Localization feature configured for this project. + NStackLocalization get localization => _nstack.localization; + + /// Gets the NStack Message feature configured for this project. + NStackMessages get messages => _nstack.messages; + @override void initState() { super.initState(); @@ -240,6 +279,212 @@ class NStackState extends State { } } +/// Listens for new messages from the NStack Messages feature. +/// +/// In where you want to use it, add this widget: +/// ```dart +/// Widget build(BuildContext context) { +/// return NStackMessageListener( +/// onMessage: (Message message) { +/// // Do whatever you want with the received message. +/// // For example, use NStackMessageDialog to display the message. +/// showDialog( +/// context: context, +/// builder: (context) { +/// return NStackMessageDialog(message: message); +/// }, +/// }, +/// child: Scaffold(...), +/// ); +/// } +/// ``` +class NStackMessageListener extends StatefulWidget { + const NStackMessageListener({ + Key? key, + required this.onMessage, + this.child, + }) : super(key: key); + + final void Function(Message message) onMessage; + final Widget? child; + + @override + State createState() => _NStackMessageListenerState(); +} + +class _NStackMessageListenerState extends State { + late final StreamSubscription _subscription; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final scope = NStackScope.of(context); + _subscription = scope.messages.onMessage.listen(widget.onMessage); + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child ?? const SizedBox(); + } +} + +typedef MessageDialogBuilder = Widget Function( + BuildContext context, + Message message, +); + +/// Default Message Alert Dialog that adapts to the platform and renders the: +/// - Message body +/// - OK Button +/// - Open URL button (if [Message.url] is provided) +/// +/// When the dialog is dismissed, the alert reports that the message is viewed. +/// +/// Use it like this: +/// ```dart +/// NStackMessageDialog.show( +/// context, +/// message: message, +/// /* Other params if needed */ +/// ); +/// ``` +class NStackMessageDialog extends StatelessWidget { + const NStackMessageDialog._({ + Key? key, + required this.message, + this.onOkPressed, + this.onOpenUrlPressed, + this.okButtonTitle = 'OK', + this.openUrlButtonTitle = 'Open URL', + this.dialogTitle = 'Message', + }) : super(key: key); + + /// Message that was received. + final Message message; + + /// Title of the OK button. + final String okButtonTitle; + + /// Title of the Open URL button. + final String openUrlButtonTitle; + + /// Title of the dialog. + final String dialogTitle; + + /// Optional callback when a user presses the OK button. + /// + /// By default, it closes the dialog and reports that the message was viewed. + final VoidCallback? onOkPressed; + + /// Optional callback when a user presses Open URL button. + /// + /// By default, it closes the dialog, reports that the message was viewed + /// and opens the URL. + final void Function(Uri uri)? onOpenUrlPressed; + + /// Displays the dialog. + static Future show( + BuildContext context, { + required Message message, + VoidCallback? onOkPressed, + void Function(Uri uri)? onOpenUrlPressed, + String okButtonTitle = 'OK', + String openUrlButtonTitle = 'Open URL', + String dialogTitle = 'Message', + }) { + builder(BuildContext context) { + return NStackMessageDialog._( + message: message, + onOkPressed: onOkPressed, + onOpenUrlPressed: onOpenUrlPressed, + okButtonTitle: okButtonTitle, + openUrlButtonTitle: openUrlButtonTitle, + dialogTitle: dialogTitle, + ); + } + + final showDialog = Platform.isIOS + ? cupertino.showCupertinoDialog(context: context, builder: builder) + : material.showDialog(context: context, builder: builder); + + return showDialog.whenComplete(() { + NStack.messages.setMessageViewed(message.id); + }); + } + + @override + Widget build(BuildContext context) { + final titleWidget = Text(dialogTitle); + final messageWidget = Text(message.message); + + final okWidget = Text(okButtonTitle); + final okAction = onOkPressed ?? Navigator.of(context).pop; + + final messageUrl = message.url; + final Uri? uri; + + if (messageUrl != null) { + uri = Uri.tryParse(messageUrl); + } else { + uri = null; + } + + final isUriValid = uri != null; + + final urlLaunchAction = !isUriValid + ? null + : (onOpenUrlPressed != null ? () => onOpenUrlPressed!(uri!) : null) ?? + () { + launchUrl(uri!); + Navigator.of(context).pop(); + }; + + if (Platform.isIOS) { + return cupertino.CupertinoAlertDialog( + title: titleWidget, + content: messageWidget, + actions: [ + cupertino.CupertinoDialogAction( + onPressed: okAction, + child: okWidget, + ), + if (isUriValid) + cupertino.CupertinoDialogAction( + isDefaultAction: true, + onPressed: urlLaunchAction, + child: Text(openUrlButtonTitle), + ), + ], + ); + } + + return material.AlertDialog( + title: titleWidget, + content: messageWidget, + actions: [ + if (isUriValid) + material.TextButton( + onPressed: urlLaunchAction, + child: Text(openUrlButtonTitle), + ), + material.TextButton( + onPressed: okAction, + child: okWidget, + ), + ], + ); + } +} + /* * * NStack Flutter Extensions From 8064b5b4c742469ee017e0b38a1e0471c2bd7058 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 18:04:20 +0300 Subject: [PATCH 09/13] feat(#51): Allow to not display the dialog title --- example/lib/nstack.dart | 6 +++--- lib/src/nstack_builder.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/lib/nstack.dart b/example/lib/nstack.dart index 93d8290..2066b7a 100644 --- a/example/lib/nstack.dart +++ b/example/lib/nstack.dart @@ -378,7 +378,7 @@ class NStackMessageDialog extends StatelessWidget { final String openUrlButtonTitle; /// Title of the dialog. - final String dialogTitle; + final String? dialogTitle; /// Optional callback when a user presses the OK button. /// @@ -399,7 +399,7 @@ class NStackMessageDialog extends StatelessWidget { void Function(Uri uri)? onOpenUrlPressed, String okButtonTitle = 'OK', String openUrlButtonTitle = 'Open URL', - String dialogTitle = 'Message', + String? dialogTitle = 'Message', }) { builder(BuildContext context) { return NStackMessageDialog._( @@ -423,7 +423,7 @@ class NStackMessageDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final titleWidget = Text(dialogTitle); + final titleWidget = dialogTitle != null ? Text(dialogTitle!) : null; final messageWidget = Text(message.message); final okWidget = Text(okButtonTitle); diff --git a/lib/src/nstack_builder.dart b/lib/src/nstack_builder.dart index 20d4ed9..b2f8197 100644 --- a/lib/src/nstack_builder.dart +++ b/lib/src/nstack_builder.dart @@ -522,7 +522,7 @@ class NStackMessageDialog extends StatelessWidget { final String openUrlButtonTitle; /// Title of the dialog. - final String dialogTitle; + final String? dialogTitle; /// Optional callback when a user presses the OK button. /// @@ -543,7 +543,7 @@ class NStackMessageDialog extends StatelessWidget { void Function(Uri uri)? onOpenUrlPressed, String okButtonTitle = 'OK', String openUrlButtonTitle = 'Open URL', - String dialogTitle = 'Message', + String? dialogTitle = 'Message', }) { builder(BuildContext context) { return NStackMessageDialog._( @@ -567,7 +567,7 @@ class NStackMessageDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final titleWidget = Text(dialogTitle); + final titleWidget = dialogTitle != null ? Text(dialogTitle!) : null; final messageWidget = Text(message.message); final okWidget = Text(okButtonTitle); From 5e32c9d612054f56359033955af6f92eb2c04cc7 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 18:34:36 +0300 Subject: [PATCH 10/13] refactor(#51): Update imports & use message translations --- lib/nstack.dart | 1 - lib/src/nstack_builder.dart | 48 ++++++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/lib/nstack.dart b/lib/nstack.dart index 40c6a23..e93f93c 100644 --- a/lib/nstack.dart +++ b/lib/nstack.dart @@ -1,2 +1 @@ export 'package:nstack/sdk/nstack_sdk.dart'; -export 'package:url_launcher/url_launcher.dart'; diff --git a/lib/src/nstack_builder.dart b/lib/src/nstack_builder.dart index b2f8197..45875eb 100644 --- a/lib/src/nstack_builder.dart +++ b/lib/src/nstack_builder.dart @@ -92,7 +92,7 @@ class NstackBuilder implements Builder { void _writeHeader(StringBuffer output) { output.writeln( ''' -// ignore_for_file: implementation_imports, non_constant_identifier_names +// ignore_for_file: implementation_imports, non_constant_identifier_names, depend_on_referenced_packages /* * ❌ GENERATED BY NSTACK, DO NOT MODIFY THIS FILE BY HAND! @@ -129,12 +129,12 @@ import 'package:nstack/models/language.dart'; import 'package:nstack/models/localize_index.dart'; import 'package:nstack/models/message.dart'; import 'package:nstack/models/nstack_config.dart'; -import 'package:nstack/src/nstack_repository.dart'; +import 'package:nstack/nstack.dart'; +import 'package:nstack/partial/section_key_delegate.dart'; import 'package:nstack/sdk/localization/nstack_localization.dart'; import 'package:nstack/sdk/messages/nstack_messages.dart'; -import 'package:nstack/partial/section_key_delegate.dart'; -import 'package:nstack/nstack.dart'; - +import 'package:nstack/src/nstack_repository.dart'; +import 'package:url_launcher/url_launcher.dart'; export 'package:nstack/models/app_open_platform.dart'; ''', @@ -313,6 +313,7 @@ const _bundledTranslations = {'''); * NStack Flutter Widgets * */ + /// Allows to access NStack features via a `BuildContext`. class NStackScope extends InheritedWidget { final NStackState state; @@ -423,6 +424,12 @@ class NStackState extends State { } } +/* + * + * NStack Messages Feature + * + */ + /// Listens for new messages from the NStack Messages feature. /// /// In where you want to use it, add this widget: @@ -481,11 +488,6 @@ class _NStackMessageListenerState extends State { } } -typedef MessageDialogBuilder = Widget Function( - BuildContext context, - Message message, -); - /// Default Message Alert Dialog that adapts to the platform and renders the: /// - Message body /// - OK Button @@ -502,14 +504,18 @@ typedef MessageDialogBuilder = Widget Function( /// ); /// ``` class NStackMessageDialog extends StatelessWidget { + static const _okButtonTitleFallback = 'OK'; + static const _openUrlButtonTitleFallback = 'Open URL'; + static const _dialogTitleFallback = 'Message'; + const NStackMessageDialog._({ Key? key, required this.message, this.onOkPressed, this.onOpenUrlPressed, - this.okButtonTitle = 'OK', - this.openUrlButtonTitle = 'Open URL', - this.dialogTitle = 'Message', + this.okButtonTitle = _okButtonTitleFallback, + this.openUrlButtonTitle = _openUrlButtonTitleFallback, + this.dialogTitle = _dialogTitleFallback, }) : super(key: key); /// Message that was received. @@ -541,17 +547,21 @@ class NStackMessageDialog extends StatelessWidget { required Message message, VoidCallback? onOkPressed, void Function(Uri uri)? onOpenUrlPressed, - String okButtonTitle = 'OK', - String openUrlButtonTitle = 'Open URL', - String? dialogTitle = 'Message', + String? okButtonTitle, + String? openUrlButtonTitle, + String? dialogTitle = _dialogTitleFallback, }) { - builder(BuildContext context) { + Widget builder(BuildContext context) { return NStackMessageDialog._( message: message, onOkPressed: onOkPressed, onOpenUrlPressed: onOpenUrlPressed, - okButtonTitle: okButtonTitle, - openUrlButtonTitle: openUrlButtonTitle, + okButtonTitle: okButtonTitle ?? + message.localization?.okBtn ?? + _okButtonTitleFallback, + openUrlButtonTitle: openUrlButtonTitle ?? + message.localization?.urlBtn ?? + _openUrlButtonTitleFallback, dialogTitle: dialogTitle, ); } From 2873dfc9805e715faa1080bf11c59620a325eaa8 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Wed, 7 Dec 2022 18:34:47 +0300 Subject: [PATCH 11/13] chore(#51): Regenerate example --- example/lib/nstack.dart | 47 +++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/example/lib/nstack.dart b/example/lib/nstack.dart index 2066b7a..a87e602 100644 --- a/example/lib/nstack.dart +++ b/example/lib/nstack.dart @@ -1,4 +1,4 @@ -// ignore_for_file: implementation_imports, non_constant_identifier_names +// ignore_for_file: implementation_imports, non_constant_identifier_names, depend_on_referenced_packages /* * ❌ GENERATED BY NSTACK, DO NOT MODIFY THIS FILE BY HAND! @@ -35,11 +35,12 @@ import 'package:nstack/models/language.dart'; import 'package:nstack/models/localize_index.dart'; import 'package:nstack/models/message.dart'; import 'package:nstack/models/nstack_config.dart'; -import 'package:nstack/src/nstack_repository.dart'; +import 'package:nstack/nstack.dart'; +import 'package:nstack/partial/section_key_delegate.dart'; import 'package:nstack/sdk/localization/nstack_localization.dart'; import 'package:nstack/sdk/messages/nstack_messages.dart'; -import 'package:nstack/partial/section_key_delegate.dart'; -import 'package:nstack/nstack.dart'; +import 'package:nstack/src/nstack_repository.dart'; +import 'package:url_launcher/url_launcher.dart'; export 'package:nstack/models/app_open_platform.dart'; @@ -157,6 +158,7 @@ class _Test extends SectionKeyDelegate { * NStack Flutter Widgets * */ + /// Allows to access NStack features via a `BuildContext`. class NStackScope extends InheritedWidget { final NStackState state; @@ -279,6 +281,12 @@ class NStackState extends State { } } +/* + * + * NStack Messages Feature + * + */ + /// Listens for new messages from the NStack Messages feature. /// /// In where you want to use it, add this widget: @@ -337,11 +345,6 @@ class _NStackMessageListenerState extends State { } } -typedef MessageDialogBuilder = Widget Function( - BuildContext context, - Message message, -); - /// Default Message Alert Dialog that adapts to the platform and renders the: /// - Message body /// - OK Button @@ -358,14 +361,18 @@ typedef MessageDialogBuilder = Widget Function( /// ); /// ``` class NStackMessageDialog extends StatelessWidget { + static const _okButtonTitleFallback = 'OK'; + static const _openUrlButtonTitleFallback = 'Open URL'; + static const _dialogTitleFallback = 'Message'; + const NStackMessageDialog._({ Key? key, required this.message, this.onOkPressed, this.onOpenUrlPressed, - this.okButtonTitle = 'OK', - this.openUrlButtonTitle = 'Open URL', - this.dialogTitle = 'Message', + this.okButtonTitle = _okButtonTitleFallback, + this.openUrlButtonTitle = _openUrlButtonTitleFallback, + this.dialogTitle = _dialogTitleFallback, }) : super(key: key); /// Message that was received. @@ -397,17 +404,21 @@ class NStackMessageDialog extends StatelessWidget { required Message message, VoidCallback? onOkPressed, void Function(Uri uri)? onOpenUrlPressed, - String okButtonTitle = 'OK', - String openUrlButtonTitle = 'Open URL', - String? dialogTitle = 'Message', + String? okButtonTitle, + String? openUrlButtonTitle, + String? dialogTitle = _dialogTitleFallback, }) { - builder(BuildContext context) { + Widget builder(BuildContext context) { return NStackMessageDialog._( message: message, onOkPressed: onOkPressed, onOpenUrlPressed: onOpenUrlPressed, - okButtonTitle: okButtonTitle, - openUrlButtonTitle: openUrlButtonTitle, + okButtonTitle: okButtonTitle ?? + message.localization?.okBtn ?? + _okButtonTitleFallback, + openUrlButtonTitle: openUrlButtonTitle ?? + message.localization?.urlBtn ?? + _openUrlButtonTitleFallback, dialogTitle: dialogTitle, ); } From 35a3324dc609a8e6c65ab3653d6f96e2530f843b Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Thu, 8 Dec 2022 12:25:42 +0300 Subject: [PATCH 12/13] feat(#51): Implement seeing a message --- lib/sdk/messages/nstack_messages.dart | 22 +++++++++++++++++--- lib/sdk/nstack_sdk.dart | 5 ++++- lib/src/nstack_repository.dart | 29 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/sdk/messages/nstack_messages.dart b/lib/sdk/messages/nstack_messages.dart index bca998d..ec0ee5f 100644 --- a/lib/sdk/messages/nstack_messages.dart +++ b/lib/sdk/messages/nstack_messages.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'package:nstack/models/app_open.dart'; import 'package:nstack/models/message.dart'; +import 'package:nstack/models/nstack_appopen_data.dart'; import 'package:nstack/src/nstack_repository.dart'; class NStackMessages { final NStackRepository _repository; + late final NStackAppOpenData _appOpenData; // There's no dispose / close method // ignore: close_sinks @@ -17,11 +19,25 @@ class NStackMessages { required NStackRepository repository, }) : _repository = repository; - Future setMessageViewed(int messageId) async { - // TODO: Set message viewed + Future setMessageViewed(int messageId) { + return _repository + .postMessageSeen( + appOpenData: _appOpenData, + messageId: messageId, + ) + .catchError((e, s) { + print( + 'NStack --> Couldnt post message seen because of: $e \n $s', + ); + }); } - void onAppOpen(AppOpen appOpen) { + void onAppOpen( + AppOpen appOpen, + NStackAppOpenData appOpenData, + ) { + _appOpenData = appOpenData; + final message = appOpen.data.message; if (message != null) { diff --git a/lib/sdk/nstack_sdk.dart b/lib/sdk/nstack_sdk.dart index 9476ee0..043fc8d 100644 --- a/lib/sdk/nstack_sdk.dart +++ b/lib/sdk/nstack_sdk.dart @@ -119,7 +119,10 @@ class NStackSdk { await localization.updateOnAppOpen(appOpen); _log('NStack --> Updated localization.'); - messages.onAppOpen(appOpen); + messages.onAppOpen( + appOpen, + _appOpenData, + ); _appOpenCalled = true; diff --git a/lib/src/nstack_repository.dart b/lib/src/nstack_repository.dart index b4653e1..aa05ecd 100644 --- a/lib/src/nstack_repository.dart +++ b/lib/src/nstack_repository.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:http/http.dart'; import 'package:nstack/models/app_open_platform.dart'; import 'package:nstack/models/localize_index.dart'; import 'package:nstack/models/nstack_appopen_data.dart'; @@ -88,4 +89,32 @@ class NStackRepository { ); return response.body; } + + Future postMessageSeen({ + required NStackAppOpenData appOpenData, + required int messageId, + }) async { + final requestBody = { + 'guid': appOpenData.guid, + 'message_id': messageId.toString(), + }; + + final url = Uri.parse('$_baseUrl/notify/messages/views'); + + final response = await http.post( + url, + headers: _headers, + body: requestBody, + ); + + if (response.statusCode != 200) { + throw NStackApiException(response); + } + } +} + +class NStackApiException { + final Response response; + + NStackApiException(this.response); } From 6e470ffa3a5d1c5aa1dac5fefc11b2d7db1d3f15 Mon Sep 17 00:00:00 2001 From: Nikita Sirovskiy Date: Fri, 9 Dec 2022 16:40:34 +0300 Subject: [PATCH 13/13] nit --- example/lib/nstack.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/example/lib/nstack.dart b/example/lib/nstack.dart index a87e602..a944725 100644 --- a/example/lib/nstack.dart +++ b/example/lib/nstack.dart @@ -441,13 +441,7 @@ class NStackMessageDialog extends StatelessWidget { final okAction = onOkPressed ?? Navigator.of(context).pop; final messageUrl = message.url; - final Uri? uri; - - if (messageUrl != null) { - uri = Uri.tryParse(messageUrl); - } else { - uri = null; - } + final uri = messageUrl != null ? Uri.tryParse(messageUrl) : null; final isUriValid = uri != null;