diff --git a/flutter-sample/README.md b/flutter-sample/README.md new file mode 100644 index 00000000..73a71bc0 --- /dev/null +++ b/flutter-sample/README.md @@ -0,0 +1,16 @@ +![Logo](/.github/logo.png) + +## MoEngage Flutter Plugin + +This repository contains the Flutter plugins for the [MoEngage](https://www.moengage.com) platform. + +### Repository Description + +| Folder | Description | +|---------|-----------------------------------------------------------------------------------| +| core | Contains the implementation for the SDK implementation for Core MoEngage Platform | +| inbox | Contains the implementation for the SDK implementation for Inbox Feature | +| example | Sample Integration for reference. | +| cards | Contains the implementation for the SDK implementation for Cards Feature | + + diff --git a/flutter-sample/analysis_options.yaml b/flutter-sample/analysis_options.yaml new file mode 100644 index 00000000..9333fd31 --- /dev/null +++ b/flutter-sample/analysis_options.yaml @@ -0,0 +1,222 @@ +# Specify analysis options. +analyzer: + language: + strict-casts: true + strict-raw-types: true + errors: + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: ignore + exclude: # DIFFERENT FROM FLUTTER/FLUTTER + # Ignore generate˚d files + - '**/*.g.dart' + - '**/*.mocks.dart' # Mockito @GenerateMocks + +linter: + rules: + # This list is derived from the list of all available lints located at + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 + - always_require_non_null_named_parameters + # - always_use_package_imports # we do this commonly + - annotate_overrides + # - avoid_annotating_with_dynamic # conflicts with always_specify_types + - avoid_bool_literals_in_conditional_expressions + # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 + # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + # - avoid_final_parameters # incompatible with prefer_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it + - avoid_print + # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters # conflicts with always_specify_types + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + # - cascade_invocations # doesn't match the typical style of this repo + - cast_nullable_to_non_nullable + # - close_sinks # not reliable enough + # - combinators_ordering # DIFFERENT FROM FLUTTER/FLUTTER: This isn't available on stable yet. + # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 + - conditional_uri_does_not_exist + # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - depend_on_referenced_packages + - deprecated_consistency + # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) + - directives_ordering + # - discarded_futures # not yet tested + # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - iterable_contains_unrelated_type + # - join_return_with_assignment # not required by flutter style + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - library_private_types_in_public_api + # - lines_longer_than_80_chars # not required by flutter style + - list_remove_unrelated_type + # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_logic_in_create_state + - no_runtimeType_toString # DIFFERENT FROM FLUTTER/FLUTTER + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + # - omit_local_variable_types # opposite of always_specify_types + # - one_member_abstracts # too many false positives + - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + # - parameter_assignments # we do this commonly + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + # - prefer_asserts_with_message # not required by flutter style + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + # - prefer_constructors_over_static_methods # far too many false positives + - prefer_contains + # - prefer_double_quotes # opposite of prefer_single_quotes + - prefer_equal_for_default_values + # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + # - prefer_final_parameters # we should enable this one day when it can be auto-fixed (https://github.com/dart-lang/linter/issues/3104), see also parameter_assignments + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + # - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 + # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs # DIFFERENT FROM FLUTTER/FLUTTER + - recursive_getters + # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 + - secure_pubspec_urls + - sized_box_for_whitespace + # - sized_box_shrink_expand # not yet tested + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies # DIFFERENT FROM FLUTTER/FLUTTER: Flutter's use case for not sorting does not apply to this repository. + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + # - type_annotate_public_apis # subset of always_specify_types + - type_init_formals + - unawaited_futures # DIFFERENT FROM FLUTTER/FLUTTER: It's disabled there for "too many false positives"; that's not an issue here, and missing awaits have caused production issues in plugins. + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_constructor_name + # - unnecessary_final # conflicts with prefer_final_locals + - unnecessary_getters_setters + # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 + - unnecessary_late + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + # - use_colored_box # not yet tested + # - use_decorated_box # not yet tested + # - use_enums # not yet tested + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 + - use_super_parameters + - use_test_throws_matchers + # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review + - valid_regexps + - void_checks \ No newline at end of file diff --git a/flutter-sample/cards/.gitignore b/flutter-sample/cards/.gitignore new file mode 100644 index 00000000..96486fd9 --- /dev/null +++ b/flutter-sample/cards/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/flutter-sample/cards/moengage_cards/CHANGELOG.md b/flutter-sample/cards/moengage_cards/CHANGELOG.md new file mode 100644 index 00000000..83118189 --- /dev/null +++ b/flutter-sample/cards/moengage_cards/CHANGELOG.md @@ -0,0 +1,18 @@ +# MoEngage Cards Plugin + +# 07-12-2023 + +## 2.1.0 +- Updated Minimum Supported `moengage_flutter` version to `6.1.0` +- Android + - Native SDK updated to support version `12.10.01` and above. + +# 13-09-2023 + +## 2.0.0 +- Federated Plugin Implementation + +# 19-07-2023 + +## 1.0.0 +- Initial Release diff --git a/flutter-sample/cards/moengage_cards/LICENSE b/flutter-sample/cards/moengage_cards/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/cards/moengage_cards/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards/README.md b/flutter-sample/cards/moengage_cards/README.md new file mode 100644 index 00000000..2fc4170e --- /dev/null +++ b/flutter-sample/cards/moengage_cards/README.md @@ -0,0 +1,161 @@ +# MoEngage Cards Plugin + +Cards Plugin for MoEngage Platform + +## SDK Installation + +To add the MoEngage Cards SDK to your application, edit your application's `pubspec.yaml` file +and add the below dependency to it: + +![Download](https://img.shields.io/pub/v/moengage_cards.svg) + +```yaml +dependencies: + moengage_cards: $latestSdkVersion +``` + +replace `$latestSdkVersion` with the latest SDK version. + +### Android Installation + +![MavenBadge](https://maven-badges.herokuapp.com/maven-central/com.moengage/cards-core/badge.svg) + +Once you install the Flutter Plugin add MoEngage's native Android SDK dependency to the Android +project of your application. +Navigate to `android --> app --> build.gradle`. Add the MoEngage Android SDK's dependency in +the `dependencies` block + +```groovy +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation("com.moengage:cards-core:$sdkVersion") +} +``` + +Run flutter packages get to install the SDK. + +## Usage + +Cards Initialization + +``` +import 'package:moengage_cards/moengage_cards.dart' as moe; +moe.MoEngageCards cards = moe.MoEngageCards(""); +cards.initialize(); +``` + +Set App Open Cards Sync Listener + +``` +cards.setAppOpenCardsSyncListener((data) { + debugPrint("Cards App Open Sync Listener: $data"); +}); +``` + +Call this API when user lands on Inbox Screen , with SyncComplete Callback + +``` +cards.onCardsSectionLoaded((data) { + debugPrint("Cards Inbox Open Sync Listener: $data"); +}); +``` + +Call this API when user dismisses Inbox Screen + +``` +cards.onCardsSectionUnLoaded(); +``` + +Notify SDK that , card is delivered. Used for Analytics Purposes + +``` +cards.cardDelivered(); +``` + +Fetch All Related Data + +``` +cards.getCardsInfo().then((cardsData) { + //Update UI +}); +``` + +Ask the SDK to refresh cards on user request. Used to mimic Pull to Refresh Behaviour + +``` +cards.refreshCards((data) { + if (data?.hasUpdates == true) { + // Refetch Cards + cards.getCardsInfo().then((cardsData) { + //Update UI + }); + } +}); +``` + +Notify SDK that card is clicked. Used for Analytics Purposes + +``` +cards.cardClicked(card, widgetId); +``` + +Notify SDK that card is Shown to the User. Used for Analytics Purposes + +``` +cards.cardShown(card); +``` + +Fetch Cards For Given Category + +``` +final category = "Promotions"; +final cardsData = await cards.getCardsForCategory(category); +``` + +Get UnClicked Cards Count + +``` +final unClickedCardsCount = await cards.getUnClickedCardsCount(); +``` + +Get New Cards Count + +``` +final newCardsCount = await cards.getNewCardsCount(); +``` + +Returns a list of categories to be shown + +``` +final cardsCategories = await cards.getCardsCategories(); +``` + +Return true if All cards category should be shown + +``` +final isAllCategoryEnabled = await cards.isAllCategoryEnabled(); +``` + +Delete Given card. + +``` +cards.deleteCard(card); +``` + +Delete Multiple Cards + +``` +final cards = []; +cards.deleteCards(cards); +``` + +Fetch Cards Data + +``` +cards.fetchCards().then((cardsData) { + //Update UI +}); +``` + +Note: This plugin is dependent on `moengage_flutter` plugin. Make sure you have installed +the `moengage_flutter` plugin as well. diff --git a/flutter-sample/cards/moengage_cards/example/README.md b/flutter-sample/cards/moengage_cards/example/README.md new file mode 100644 index 00000000..d8d3a70c --- /dev/null +++ b/flutter-sample/cards/moengage_cards/example/README.md @@ -0,0 +1,3 @@ +# moengage_cards_example + +Demonstrates how to use the moengage_cards plugin. diff --git a/flutter-sample/cards/moengage_cards/lib/moengage_cards.dart b/flutter-sample/cards/moengage_cards/lib/moengage_cards.dart new file mode 100644 index 00000000..cf57f22a --- /dev/null +++ b/flutter-sample/cards/moengage_cards/lib/moengage_cards.dart @@ -0,0 +1,3 @@ +/// Export MoEngage Cards Module Public Classes +export 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; +export 'src/moengage_cards.dart'; diff --git a/flutter-sample/cards/moengage_cards/lib/src/moengage_cards.dart b/flutter-sample/cards/moengage_cards/lib/src/moengage_cards.dart new file mode 100644 index 00000000..e1a45e7e --- /dev/null +++ b/flutter-sample/cards/moengage_cards/lib/src/moengage_cards.dart @@ -0,0 +1,131 @@ +import 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; +import 'package:moengage_flutter/moengage_flutter.dart'; + +/// Helper Class to interact with MoEngage Cards Feature +class MoEngageCards { + /// [MoEngageCards] Constructor + MoEngageCards(this._appId) { + /// Requires for Setting up Native to Flutter Method Channel Callbacks + CardsController.init(); + } + + static const String _tag = '${moduleTag}MoEngageCards'; + + /// Account identifier, APP ID on the MoEngage Dashboard. + final String _appId; + + /// Cards Plugin Platform Interface + final MoEngageCardsPlatformInterface _cardsPlatform = + MoEngageCardsPlatformInterface.instance; + + /// Initialize the cards module + void initialize() { + Logger.v('$_tag initialize(): Initializing Cards Module'); + _cardsPlatform.initialize(_appId); + } + + /// Ask the SDK to refresh cards on user request + /// Note: This API is to be used to mimic the pull to refresh behaviour + /// [cardsSyncListener] - Callback for Card Sync Completion of type [CardsSyncListener] + void refreshCards(CardsSyncListener cardsSyncListener) { + Logger.v('$_tag refreshCards(): Refresh Cards'); + _cardsPlatform.refreshCards(_appId, cardsSyncListener); + } + + /// Returns all Cards data of type [CardsData] + /// Note: This API refreshes cards data if inbox sync time interval expired + Future fetchCards() { + Logger.v('$_tag fetchCards(): Fetch Cards'); + return _cardsPlatform.fetchCards(_appId); + } + + /// Notify the MoEngage SDK that card section has loaded + /// Note: This API should be used when the cards/inbox screen is visible to user + /// [cardsSyncListener] - Callback for Card Sync Completion of type [CardsSyncListener] + void onCardsSectionLoaded(CardsSyncListener cardsSyncListener) { + Logger.v('$_tag onCardsSectionLoaded(): '); + _cardsPlatform.onCardsSectionLoaded(_appId, cardsSyncListener); + } + + /// Notifies the SDK that the inbox view is gone to the background + /// Note: This API should be used when the cards screen is invisible to user + void onCardsSectionUnLoaded() { + Logger.v('$_tag onCardsSectionUnLoaded(): '); + _cardsPlatform.onCardsSectionUnLoaded(_appId); + } + + /// Returns a list of categories to be shown + /// Returns [List] of [String] data as [Future] + Future> getCardsCategories() async { + Logger.v('$_tag getCardsCategories(): Fetching list of Cards Categories'); + return _cardsPlatform.getCardsCategories(_appId); + } + + /// Fetches all cards related data + /// Returns [CardsInfo] Data as [Future] + Future getCardsInfo() async { + Logger.v('$_tag getCardsInfo(): Get Cards Related Info'); + return _cardsPlatform.getCardsInfo(_appId); + } + + /// Marks a card as clicked and tracks an event for statistical purpose + /// [card] - Instance of [Card] + /// [widgetId] - Id of Widget , on which click action is performed + void cardClicked(Card card, int widgetId) { + Logger.v('$_tag cardClicked(): WidgetId - $widgetId'); + _cardsPlatform.cardClicked(card, widgetId, _appId); + } + + /// Notify SDK that the card is delivered. + /// Used for statistical purpose + void cardDelivered() { + _cardsPlatform.cardDelivered(_appId); + } + + /// Notify SDK the card is shown to the user - Used for statistical purposes + /// It is recommended to call this API in initState in Card Stateful Widget + /// [card] - Instance of [Card] + void cardShown(Card card) { + _cardsPlatform.cardShown(card, _appId); + } + + /// Fetch Cards for given [category] + /// Returns [CardsData] as [Future] + Future getCardsForCategory(String category) { + return _cardsPlatform.getCardsForCategory(category, _appId); + } + + /// Deletes the given card + /// [card] - Instance of [Card] + void deleteCard(Card card) { + deleteCards([card]); + } + + /// Deletes multiple cards at same time. + /// [card] - List of [Card] object + Future deleteCards(List cards) async { + await _cardsPlatform.deleteCards(cards, _appId); + } + + /// Return true if All cards category should be shown + Future isAllCategoryEnabled() { + return _cardsPlatform.isAllCategoryEnabled(_appId); + } + + /// Return count of new cards available + /// Returns [int] cards count as [Future] + Future getNewCardsCount() { + return _cardsPlatform.getNewCardsCount(_appId); + } + + /// Return count of UnClicked cards + Future getUnClickedCardsCount() async { + return _cardsPlatform.getUnClickedCardsCount(_appId); + } + + /// Listener for Cards App Open Sync Listener + /// [cardsSyncListener] - Callback for Card Sync Completion of type [CardsSyncListener] + void setAppOpenCardsSyncListener(CardsSyncListener cardsSyncListener) { + _cardsPlatform.setAppOpenCardsSyncListener(cardsSyncListener, _appId); + } +} diff --git a/flutter-sample/cards/moengage_cards/pubspec.yaml b/flutter-sample/cards/moengage_cards/pubspec.yaml new file mode 100644 index 00000000..d9e855cd --- /dev/null +++ b/flutter-sample/cards/moengage_cards/pubspec.yaml @@ -0,0 +1,31 @@ +name: moengage_cards +description: MoEngage Cards Plugin +version: 2.1.0 +homepage: https://moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + platforms: + android: + default_package: moengage_cards_android + ios: + default_package: moengage_cards_ios + +dependencies: + flutter: + sdk: flutter + moengage_cards_android: ^1.1.0 + moengage_cards_ios: ^1.1.0 + moengage_cards_platform_interface: ^1.1.0 + moengage_flutter: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.3.0 + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/cards/moengage_cards/pubspec_overrides.yaml b/flutter-sample/cards/moengage_cards/pubspec_overrides.yaml new file mode 100644 index 00000000..5c6ab503 --- /dev/null +++ b/flutter-sample/cards/moengage_cards/pubspec_overrides.yaml @@ -0,0 +1,17 @@ +dependency_overrides: + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_cards_platform_interface: + path: ../moengage_cards_platform_interface/ + moengage_cards_android: + path: ../moengage_cards_android/ + moengage_cards_ios: + path: ../moengage_cards_ios/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards/test/moengage_cards_test.dart b/flutter-sample/cards/moengage_cards/test/moengage_cards_test.dart new file mode 100644 index 00000000..1f508431 --- /dev/null +++ b/flutter-sample/cards/moengage_cards/test/moengage_cards_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); +} diff --git a/flutter-sample/cards/moengage_cards_android/CHANGELOG.md b/flutter-sample/cards/moengage_cards_android/CHANGELOG.md new file mode 100644 index 00000000..62fc3cca --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/CHANGELOG.md @@ -0,0 +1,14 @@ +# MoEngage Cards Android Plugin + +# 07-12-2023 + +# 1.1.0 +- Add support for AGP `8.0.2` and above +- Upgrade Kotlin Version to `1.7.10` +- Support for `cards-core` version `1.6.0` and above +- Fix: Android to Dart Method Channel Communication Breaking when `FirebaseMessaging.onBackgroundMessage` is used + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/LICENSE b/flutter-sample/cards/moengage_cards_android/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/README.md b/flutter-sample/cards/moengage_cards_android/README.md new file mode 100644 index 00000000..f54dca10 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/README.md @@ -0,0 +1,15 @@ +# moengage\_cards\_android + +The Android implementation of [`moengage_cards`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_cards` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_cards +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/android/.gitignore b/flutter-sample/cards/moengage_cards_android/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/flutter-sample/cards/moengage_cards_android/android/build.gradle b/flutter-sample/cards/moengage_cards_android/android/build.gradle new file mode 100644 index 00000000..253152ad --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/build.gradle @@ -0,0 +1,56 @@ +group 'com.meongage.flutter_cards' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.0.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + mavenLocal() + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdk 33 + namespace "com.moengage.flutter.cards" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } +} + +dependencies { + + api("com.moengage:plugin-base-cards:1.1.0") + compileOnly("com.moengage:moe-android-sdk:12.10.01") + compileOnly("com.moengage:plugin-base:3.4.0") + compileOnly("com.moengage:cards-core:1.6.0") +} \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/android/gradle.properties b/flutter-sample/cards/moengage_cards_android/android/gradle.properties new file mode 100644 index 00000000..c7e2444a --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M +android.defaults.buildfeatures.buildconfig=true \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/android/gradle/wrapper/gradle-wrapper.properties b/flutter-sample/cards/moengage_cards_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a403c412 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/android/settings.gradle b/flutter-sample/cards/moengage_cards_android/android/settings.gradle new file mode 100644 index 00000000..3154a65b --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "moengage_cards" + diff --git a/flutter-sample/cards/moengage_cards_android/android/src/main/AndroidManifest.xml b/flutter-sample/cards/moengage_cards_android/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4ac1e09b --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/Constants.kt b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/Constants.kt new file mode 100644 index 00000000..47ee477d --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/Constants.kt @@ -0,0 +1,24 @@ +package com.moengage.flutter.cards + +internal const val CHANNEL_NAME = "com.moengage/cards" +internal const val MODULE_TAG = "MoEFlutterCards_" + +internal const val METHOD_INITIALIZE = "initialize" +internal const val METHOD_REFRESH_CARDS = "refreshCards" +internal const val METHOD_ON_CARD_SECTION_LOADED = "onCardSectionLoaded" +internal const val METHOD_ON_CARD_SECTION_UNLOADED = "onCardSectionUnLoaded" +internal const val METHOD_GET_CARDS_CATEGORIES = "getCardsCategories" +internal const val METHOD_CARDS_INFO = "getCardsInfo" +internal const val METHOD_CARD_CLICKED = "cardClicked" +internal const val METHOD_CARD_DELIVERED = "cardDelivered" +internal const val METHOD_CARD_SHOWN = "cardShown" +internal const val METHOD_CARDS_FOR_CATEGORY = "cardsForCategory" +internal const val METHOD_DELETE_CARDS = "deleteCards" +internal const val METHOD_IS_ALL_CATEGORY_ENABLED = "isAllCategoryEnabled" +internal const val METHOD_NEW_CARDS_COUNT = "getNewCardsCount" +internal const val METHOD_UN_CLICKED_CARDS_COUNT = "unClickedCardsCount" +internal const val METHOD_FETCH_CARDS = "fetchCards" + +internal const val METHOD_INBOX_OPEN_CARDS_SYNC = "onInboxOpenCardsSync" +internal const val METHOD_PULL_TO_REFRESH_CARDS_SYNC = "onPullToRefreshCardsSync" +internal const val METHOD_APP_OPEN_CARDS_SYNC = "onAppOpenCardsSync" \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/EventEmitterImpl.kt b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/EventEmitterImpl.kt new file mode 100644 index 00000000..2c3a7e40 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/EventEmitterImpl.kt @@ -0,0 +1,55 @@ +package com.moengage.flutter.cards + +import com.moengage.core.LogLevel +import com.moengage.core.internal.logger.Logger +import com.moengage.plugin.base.cards.internal.CardsEventEmitter +import com.moengage.plugin.base.cards.internal.cardsSyncToJson +import com.moengage.plugin.base.cards.internal.model.events.CardEventType +import com.moengage.plugin.base.cards.internal.model.events.CardsEvent +import com.moengage.plugin.base.cards.internal.model.events.CardsSyncEvent +import org.json.JSONObject + +class EventEmitterImpl(private val callBack: (methodName: String, payload: String) -> Unit) : + CardsEventEmitter { + private val tag = "${MODULE_TAG}EventEmitterImpl" + + + override fun emit(event: CardsEvent) { + try { + when (event) { + is CardsSyncEvent -> emitCardSyncEvent(event) + else -> Logger.print(LogLevel.ERROR) { "$tag emit() Unknown Event: $event" } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "emit(): $event" } + } + } + + private fun emitCardSyncEvent(event: CardsSyncEvent) { + try { + val syncCompleteData = event.syncCompleteData + if (syncCompleteData == null) { + Logger.print(LogLevel.ERROR) { "emitCardSyncEvent(): $event : Sync CompleteData is null" } + } + val syncCompleteJson = cardsSyncToJson(syncCompleteData, event.accountMeta) + val method = when (event.cardEventType) { + CardEventType.APP_OPEN_SYNC -> METHOD_APP_OPEN_CARDS_SYNC + CardEventType.INBOX_OPEN_SYNC -> METHOD_INBOX_OPEN_CARDS_SYNC + CardEventType.PULL_TO_REFRESH_SYNC -> METHOD_PULL_TO_REFRESH_CARDS_SYNC + } + emit(method, syncCompleteJson) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitCardSyncEvent(): $event" } + } + } + + private fun emit(methodName: String, payload: JSONObject) { + try { + Logger.print { "$tag emit() : methodName: $methodName , payload: $payload" } + callBack.invoke(methodName, payload.toString()) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emit() : " } + } + } + +} diff --git a/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/MoEngageCardsPlugin.kt b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/MoEngageCardsPlugin.kt new file mode 100644 index 00000000..c163d2ee --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/MoEngageCardsPlugin.kt @@ -0,0 +1,125 @@ +package com.moengage.flutter.cards + +import android.content.Context +import com.moengage.core.LogLevel +import com.moengage.core.internal.global.GlobalResources +import com.moengage.core.internal.logger.Logger +import com.moengage.plugin.base.cards.CardsPluginHelper +import com.moengage.plugin.base.cards.internal.setCardsEventEmitter +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodChannel + +class MoEngageCardsPlugin : FlutterPlugin, ActivityAware { + + private val tag = "${MODULE_TAG}MoEngageCardsPlugin" + + private val cardsPluginHelper: CardsPluginHelper by lazy { CardsPluginHelper() } + + lateinit var context: Context + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + try { + Logger.print { "$tag onAttachedToEngine() : Registering MoEngageCardsPlugin" } + context = binding.applicationContext + flutterPluginBinding = binding + if (methodChannel == null) { + initPlugin(binding.binaryMessenger) + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag onAttachedToEngine() : " } + } + } + + private fun initPlugin(binaryMessenger: BinaryMessenger) { + try { + Logger.print { "$tag initPlugin(): Initializing MoEngage Cards Plugin" } + methodChannel = MethodChannel(binaryMessenger, CHANNEL_NAME) + methodChannel?.setMethodCallHandler( + PlatformMethodCallHandler( + context, + cardsPluginHelper + ) + ) + setCardsEventEmitter(EventEmitterImpl(::emitEvent)) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag initPlugin() : " } + } + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + try { + Logger.print { "$tag onDetachedFromEngine() : Detaching the Framework" } + cardsPluginHelper.onFrameworkDetached() + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag onDetachedFromEngine() : " } + } + } + + private fun emitEvent(methodName: String, payload: String) { + try { + GlobalResources.mainThread.post { + try { + methodChannel?.invokeMethod(methodName, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitEvent() : " } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitEvent() : " } + } + } + + /** + * Called when the plugin is attached to Flutter Activity. + * @param binding instance of [ActivityPluginBinding] + */ + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + Logger.print { "$tag onAttachedToActivity() : Attached To Activity" } + flutterPluginBinding?.binaryMessenger?.let { + initPlugin(it) + } + } + + /** + * Called when the plugin is Detached From Flutter Activity. + */ + override fun onDetachedFromActivity() { + Logger.print { "$tag onDetachedFromActivity() : Resetting methodChannel to `null`" } + methodChannel = null + } + + /** + * Called when the plugin is Detached From Flutter Activity for Config Changes + */ + override fun onDetachedFromActivityForConfigChanges() { + Logger.print { + "$tag onDetachedFromActivityForConfigChanges() : Detached From Activity for Config changes" + } + } + + /** + * Called when the plugin is Reattached to Flutter Activity For Config Changes. + * @param binding instance of [ActivityPluginBinding] + */ + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + Logger.print { + "$tag onReattachedToActivityForConfigChanges() : ReAttached To Activity for Config changes" + } + } + + + companion object { + /** + * Static MethodChannel instance to avoid plugin reinitializing from Background Isolate + */ + internal var methodChannel: MethodChannel? = null + + /** + * Instance of [FlutterPluginBinding] to reinitialize the Method Channel on [onAttachedToActivity] + */ + internal var flutterPluginBinding: FlutterPluginBinding? = null + } +} diff --git a/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/PlatformMethodCallHandler.kt b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/PlatformMethodCallHandler.kt new file mode 100644 index 00000000..cea73a2c --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/src/main/kotlin/com/moengage/flutter/cards/PlatformMethodCallHandler.kt @@ -0,0 +1,278 @@ +package com.moengage.flutter.cards + +import android.content.Context +import com.moengage.cards.core.model.CardData +import com.moengage.core.LogLevel +import com.moengage.core.internal.global.GlobalResources +import com.moengage.core.internal.logger.Logger +import com.moengage.plugin.base.cards.CardsPluginHelper +import com.moengage.plugin.base.cards.internal.cardDataToJson +import com.moengage.plugin.base.internal.instanceMetaFromJson +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.json.JSONObject + +class PlatformMethodCallHandler( + private val context: Context, + private val cardsPluginHelper: CardsPluginHelper +) : MethodChannel.MethodCallHandler { + + private val tag = "${MODULE_TAG}PlatformMethodCallHandler" + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + try { + if (call.arguments == null) { + Logger.print(LogLevel.ERROR) { "$tag onMethodCall() ${call.method}: Arguments null" } + return + } + Logger.print { "$tag onMethodCall() : Method: ${call.method}" } + when (call.method) { + METHOD_INITIALIZE -> initialize(call) + METHOD_REFRESH_CARDS -> refreshCards(call) + METHOD_ON_CARD_SECTION_LOADED -> onCardsSectionLoaded(call) + METHOD_ON_CARD_SECTION_UNLOADED -> onCardsSectionUnLoaded(call) + METHOD_CARDS_INFO -> getCardsInfo(call, result) + METHOD_GET_CARDS_CATEGORIES -> getCardsCategories(call, result) + METHOD_CARD_CLICKED -> cardClicked(call) + METHOD_CARD_DELIVERED -> cardDelivered(call) + METHOD_CARD_SHOWN -> cardShown(call) + METHOD_CARDS_FOR_CATEGORY -> getCardsForCategory(call, result) + METHOD_DELETE_CARDS -> deleteCards(call) + METHOD_IS_ALL_CATEGORY_ENABLED -> isAllCategoryEnabled(call, result) + METHOD_NEW_CARDS_COUNT -> getNewCardsCount(call, result) + METHOD_UN_CLICKED_CARDS_COUNT -> getUnClickedCardsCount(call, result) + METHOD_FETCH_CARDS -> fetchCards(call, result) + else -> { + Logger.print(LogLevel.ERROR) { "$tag onMethodCall() : Method Not supported : ${call.method}" } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag onMethodCall() : " } + } + } + + + private fun initialize(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag initialize() : MoEngage Cards plugin initialised. $payload" } + cardsPluginHelper.initialise(payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag initialize() : " } + } + } + + private fun refreshCards(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag refreshCards() : $payload" } + cardsPluginHelper.refreshCards(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag refreshCards() : " } + } + } + + private fun onCardsSectionLoaded(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag onCardsSectionLoaded() : $payload" } + cardsPluginHelper.onCardSectionLoaded(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag onCardsSectionLoaded() : " } + } + } + + private fun onCardsSectionUnLoaded(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag onCardsSectionUnLoaded() : $payload" } + cardsPluginHelper.onCardSectionUnLoaded(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag onCardsSectionUnLoaded() : " } + } + } + + private fun cardClicked(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag cardClicked() : $payload" } + cardsPluginHelper.cardClicked(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag cardClicked() : " } + } + } + + private fun cardDelivered(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag cardDelivered() : $payload" } + cardsPluginHelper.cardDelivered(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag cardDelivered() : " } + } + } + + private fun cardShown(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag cardShown() : $payload" } + cardsPluginHelper.cardShown(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag cardShown() : " } + } + } + + private fun deleteCards(call: MethodCall) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag deleteCards() : $payload" } + cardsPluginHelper.deleteCards(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR) { "$tag deleteCards() : " } + } + } + + private fun getCardsInfo(call: MethodCall, result: MethodChannel.Result) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag getCardsInfo() : $payload" } + GlobalResources.executor.submit { + val cardsInfo = cardsPluginHelper.getCardsInfo(context, payload) + GlobalResources.mainThread.post { + try { + Logger.print { "$tag getCardsInfo(): Result : $cardsInfo" } + result.success(cardsInfo) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getCardsInfo() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getCardsInfo() : " } + } + } + + private fun fetchCards(call: MethodCall, result: MethodChannel.Result) { + val payload = call.arguments.toString() + try { + Logger.print { "$tag fetchCards() : $payload" } + GlobalResources.executor.submit { + cardsPluginHelper.fetchCards(context, payload, cardAvailableListener = { + GlobalResources.mainThread.post { + Logger.print { "$tag fetchCards(): Result Success: $it" } + result.success(getCardPayload(it, payload).toString()) + } + }) + } + } catch (t: Throwable) { + result.success(getCardPayload(null, payload).toString()) + Logger.print(LogLevel.ERROR, t) { "$tag fetchCards() : " } + } + } + + private fun getCardsCategories(call: MethodCall, result: MethodChannel.Result) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag getCardsCategories() : $payload" } + GlobalResources.executor.submit { + val categories = cardsPluginHelper.getCardsCategories(context, payload) + GlobalResources.mainThread.post { + try { + Logger.print { "$tag getCardsCategories(): Result : $categories" } + result.success(categories) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getCardsCategories() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getCardsCategories() : " } + } + + } + + private fun getCardsForCategory(call: MethodCall, result: MethodChannel.Result) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag getCardsForCategory() : $payload" } + GlobalResources.executor.submit { + val cards = cardsPluginHelper.getCardsForCategory(context, payload) + GlobalResources.mainThread.post { + try { + Logger.print { "$tag getCardsForCategory(): Result : $cards" } + result.success(cards) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getCardsCategories() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getCardsForCategory() : " } + } + } + + + private fun isAllCategoryEnabled(call: MethodCall, result: MethodChannel.Result) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag isAllCategoryEnabled() : $payload" } + GlobalResources.executor.submit { + val isAllCategoryEnabled = cardsPluginHelper.isAllCategoryEnabled(context, payload) + GlobalResources.mainThread.post { + try { + Logger.print { "$tag isAllCategoryEnabled(): Result : $isAllCategoryEnabled" } + result.success(isAllCategoryEnabled) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag isAllCategoryEnabled() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag isAllCategoryEnabled() : " } + } + } + + private fun getNewCardsCount(call: MethodCall, result: MethodChannel.Result) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag getNewCardsCount() : $payload" } + GlobalResources.executor.submit { + val newCardsCountResult = cardsPluginHelper.getNewCardsCount(context, payload) + GlobalResources.mainThread.post { + try { + Logger.print { "$tag getNewCardsCount(): Result : $newCardsCountResult" } + result.success(newCardsCountResult) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getNewCardsCount() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getNewCardsCount() : " } + } + } + + private fun getUnClickedCardsCount(call: MethodCall, result: MethodChannel.Result) { + try { + val payload = call.arguments.toString() + Logger.print { "$tag getUnClickedCardsCount() : $payload" } + GlobalResources.executor.submit { + val unClickedCardsCount = cardsPluginHelper.getUnClickedCardsCount(context, payload) + GlobalResources.mainThread.post { + try { + Logger.print { "$tag getUnClickedCardsCount(): Result : $unClickedCardsCount" } + result.success(unClickedCardsCount) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getUnClickedCardsCount() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getUnClickedCardsCount() : " } + } + } + + private fun getCardPayload(cardData: CardData?, payload: String): JSONObject { + return cardDataToJson(cardData, instanceMetaFromJson(JSONObject(payload))) + } +} \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/android/user-agent.gradle b/flutter-sample/cards/moengage_cards_android/android/user-agent.gradle new file mode 100644 index 00000000..a94080a7 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/android/user-agent.gradle @@ -0,0 +1,19 @@ +import java.util.regex.Matcher +import java.util.regex.Pattern + +String libraryVersionName = "UNKNOWN" +File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml') + +if (pubspec.exists()) { + String yaml = pubspec.text + // Using \s*['|"]?([^\n|'|"]*)['|"]? to extract version number. + Matcher versionMatcher = Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", Pattern.MULTILINE).matcher(yaml) + if (versionMatcher.find()) libraryVersionName = versionMatcher.group(1).replaceAll("\\+", "-") +} + +android { + defaultConfig { + // BuildConfig.VERSION_NAME + buildConfigField("String", 'MOENGAGE_CARDS_FLUTTER_LIBRARY_VERSION', "\"${libraryVersionName}\"") + } +} diff --git a/flutter-sample/cards/moengage_cards_android/lib/moengage_cards_android.dart b/flutter-sample/cards/moengage_cards_android/lib/moengage_cards_android.dart new file mode 100644 index 00000000..df801798 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/lib/moengage_cards_android.dart @@ -0,0 +1,143 @@ +import 'dart:convert'; + +import 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' + show Logger, getAccountMeta; + +/// Android Implementation for Cards Platform Interface +class MoEngageCardsAndroid extends MoEngageCardsPlatform { + /// Registers this class as the default instance of [MoEngageCardsPlatformInterface] + static void registerWith() { + Logger.v('Registering MoEngageCardsAndroid with Platform Interface'); + MoEngageCardsPlatformInterface.instance = MoEngageCardsAndroid(); + } + + @override + void initialize(String appId) { + methodChannel.invokeMethod( + methodInitialize, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + void refreshCards(String appId, CardsSyncListener cardsSyncListener) { + super.refreshCards(appId, cardsSyncListener); + methodChannel.invokeMethod( + methodRefreshCards, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + Future fetchCards(String appId) async { + final result = await methodChannel.invokeMethod( + methodFetchCards, + getAccountMeta(appId), + ); + return deSerializeCardsData(result.toString()); + } + + @override + void onCardsSectionLoaded(String appId, CardsSyncListener cardsSyncListener) { + super.onCardsSectionLoaded(appId, cardsSyncListener); + methodChannel.invokeMethod( + methodOnCardSectionLoaded, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + void onCardsSectionUnLoaded(String appId) { + methodChannel.invokeMethod( + methodOnCardSectionUnLoaded, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + Future> getCardsCategories(String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsCategories, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeCardsCategories(result.toString()); + } + + @override + Future getCardsInfo(String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsInfo, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeCardsInfo(result.toString()); + } + + @override + void cardClicked(Card card, int widgetId, String appId) { + methodChannel.invokeMethod( + methodCardClicked, + jsonEncode(getCardClickPayload(card, widgetId, appId)), + ); + } + + @override + void cardDelivered(String appId) { + methodChannel.invokeMethod( + methodCardDelivered, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + void cardShown(Card card, String appId) { + methodChannel.invokeMethod( + methodCardShown, + jsonEncode(getCardShownPayload(card, appId)), + ); + } + + @override + Future getCardsForCategory(String category, String appId) async { + final dynamic result = await methodChannel.invokeMethod( + methodCardsForCategory, + jsonEncode(getCardsForCategoryPayload(category, appId)), + ); + return deSerializeCardsData(result.toString()); + } + + @override + Future deleteCards(List cards, String appId) async { + await methodChannel.invokeMethod( + methodDeleteCards, + jsonEncode(getDeleteCardsPayload(cards, appId)), + ); + } + + @override + Future isAllCategoryEnabled(String appId) async { + final dynamic result = await methodChannel.invokeMethod( + methodIsAllCategoryEnabled, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeIsAllCategoryEnabled(result.toString()); + } + + @override + Future getNewCardsCount(String appId) async { + final dynamic result = await methodChannel.invokeMethod( + methodNewCardsCount, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeNewCardsCount(result.toString()); + } + + @override + Future getUnClickedCardsCount(String appId) async { + final dynamic result = await methodChannel.invokeMethod( + methodUnClickedCardsCount, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeUnClickedCardsCount(result.toString()); + } +} diff --git a/flutter-sample/cards/moengage_cards_android/pubspec.yaml b/flutter-sample/cards/moengage_cards_android/pubspec.yaml new file mode 100644 index 00000000..78b95159 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/pubspec.yaml @@ -0,0 +1,29 @@ +name: moengage_cards_android +description: Android implementation of the moengage_cards plugin +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_cards + platforms: + android: + package: com.moengage.flutter.cards + pluginClass: MoEngageCardsPlugin + dartPluginClass: MoEngageCardsAndroid + +dependencies: + flutter: + sdk: flutter + moengage_cards_platform_interface: ^1.1.0 + moengage_flutter: ^6.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/cards/moengage_cards_android/pubspec_overrides.yaml b/flutter-sample/cards/moengage_cards_android/pubspec_overrides.yaml new file mode 100644 index 00000000..05bb2fe3 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/pubspec_overrides.yaml @@ -0,0 +1,13 @@ +dependency_overrides: + moengage_cards_platform_interface: + path: ../moengage_cards_platform_interface/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_android/test/moengage_cards_android_test.dart b/flutter-sample/cards/moengage_cards_android/test/moengage_cards_android_test.dart new file mode 100644 index 00000000..1f508431 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_android/test/moengage_cards_android_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); +} diff --git a/flutter-sample/cards/moengage_cards_ios/CHANGELOG.md b/flutter-sample/cards/moengage_cards_ios/CHANGELOG.md new file mode 100644 index 00000000..f9767eab --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/CHANGELOG.md @@ -0,0 +1,11 @@ +# MoEngage Cards iOS Plugin + +# 01-12-2023 + +## 1.1.0 +- Updated MoEngageCards to 4.13.0 + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_ios/LICENSE b/flutter-sample/cards/moengage_cards_ios/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_ios/README.md b/flutter-sample/cards/moengage_cards_ios/README.md new file mode 100644 index 00000000..a8edce65 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/README.md @@ -0,0 +1,15 @@ +# moengage\_cards\_ios + +The iOS implementation of [`moengage_cards`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_cards` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_cards +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_ios/ios/.gitignore b/flutter-sample/cards/moengage_cards_ios/ios/.gitignore new file mode 100644 index 00000000..0c885071 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_ios/ios/Assets/.gitkeep b/flutter-sample/cards/moengage_cards_ios/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardSyncListner.swift b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardSyncListner.swift new file mode 100644 index 00000000..86db4ee0 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardSyncListner.swift @@ -0,0 +1,44 @@ +// +// MoEngageCardSyncListner.swift +// moengage_cards +// +// Created by Soumya Mahunt on 22/06/23. +// + +import Flutter +import MoEngagePluginCards + +class MoEngageCardSyncListner: MoEngageCardSyncDelegate { + private let channel: FlutterMethodChannel + + init(producingOnChannel channel: FlutterMethodChannel) { + self.channel = channel + } + + func syncComplete( + forEventType eventType: MoEngageCardsSyncEventType, + withData data: [String : Any] + ) { + let jsonStr = MoEngageCardsUtil.serialize(data: data) + MoEngagePluginCardsLogger.debug( + "Got sync update data \(data) for sync type \(eventType)", + forData: data + ) + DispatchQueue.main.async { + self.channel.invokeMethod(eventType.mappedMethodName, arguments: jsonStr) + } + } +} + +extension MoEngageCardsSyncEventType { + var mappedMethodName: String { + switch self { + case .pullToRefresh: + return MoEngageFlutterCardsConstants.NativeToFlutterMethods.pullToRefreshCardsSync + case .inboxOpen: + return MoEngageFlutterCardsConstants.NativeToFlutterMethods.inboxOpenCardsSync + case .appOpen: + return MoEngageFlutterCardsConstants.NativeToFlutterMethods.appOpenCardsSync + } + } +} diff --git a/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardsPlugin.swift b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardsPlugin.swift new file mode 100644 index 00000000..486cb5ae --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardsPlugin.swift @@ -0,0 +1,123 @@ +import Flutter +import UIKit +import MoEngageCore +import MoEngagePluginCards + +public class MoEngageCardsPlugin: NSObject, FlutterPlugin { + private let pluginHelper = MoEngagePluginCardsBridge.sharedInstance + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: MoEngageFlutterCardsConstants.pluginChannelName, + binaryMessenger: registrar.messenger() + ) + + let pluginInstance = MoEngageCardsPlugin() + registrar.addMethodCallDelegate(pluginInstance, channel: channel) + pluginInstance.pluginHelper.setSyncEventListnerDelegate( + MoEngageCardSyncListner(producingOnChannel: channel) + ) + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + pluginHelper.onFrameworkDetached() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let payload = call.arguments as? [String: Any] else { + MoEngagePluginCardsLogger.error( + "Failed to capture flutter method channel arguments for method " + + "\(call.method) and data \(String(describing: call.arguments))" + ) + return + } + + MoEngagePluginCardsLogger.debug( + "Got data \(payload) from client for channel method \(call.method)", + forData: payload + ) + + switch call.method { + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.initialize: + pluginHelper.initialize(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.refreshCards: + pluginHelper.refreshCards(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.fetchCards: + pluginHelper.fetchCards(payload) { data in + MoEngageCardsUtil.resume( + channel: call.method, + havingResult: result, + withData: data + ) + } + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.onCardSectionLoaded: + pluginHelper.onCardsSectionLoaded(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.setAppOpenCardsSyncListener: + pluginHelper.setAppOpenSyncListener(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.onCardSectionUnloaded: + pluginHelper.onCardsSectionUnLoaded(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.getCardsCategories: + pluginHelper.getCardsCategories(payload) { data in + MoEngageCardsUtil.resume( + channel: call.method, + havingResult: result, + withData: data + ) + } + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.cardsInfo: + pluginHelper.getCardsInfo(payload) { data in + MoEngageCardsUtil.resume( + channel: call.method, + havingResult: result, + withData: data + ) + } + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.cardClicked: + pluginHelper.cardClicked(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.cardDelivered: + pluginHelper.cardDelivered(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.cardShown: + pluginHelper.cardShown(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.cardsForCategory: + pluginHelper.getCardsForCategory(payload) { data in + MoEngageCardsUtil.resume( + channel: call.method, + havingResult: result, + withData: data + ) + } + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.deleteCards: + pluginHelper.deleteCards(payload) + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.isAllCategoryEnabled: + pluginHelper.isAllCategoryEnabled(payload) { data in + MoEngageCardsUtil.resume( + channel: call.method, + havingResult: result, + withData: data + ) + } + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.newCardsCount: + pluginHelper.getNewCardsCount(payload) { data in + MoEngageCardsUtil.resume( + channel: call.method, + havingResult: result, + withData: data + ) + } + case MoEngageFlutterCardsConstants.FlutterToNativeMethods.unClickedCardsCount: + pluginHelper.getUnClickedCardsCount(payload) { data in + MoEngageCardsUtil.resume( + channel: call.method, + havingResult: result, + withData: data + ) + } + default: + MoEngagePluginCardsLogger.error( + "Flutter method channel not handled for method " + + "\(call.method) and data \(payload)", + forData: payload + ) + } + } +} diff --git a/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardsUtil.swift b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardsUtil.swift new file mode 100644 index 00000000..3e810a2a --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageCardsUtil.swift @@ -0,0 +1,33 @@ +// +// MoEngageCardsUtil.swift +// moengage_cards +// +// Created by Soumya Mahunt on 26/06/23. +// + +import Flutter +import MoEngagePluginCards + +enum MoEngageCardsUtil { + static func resume( + channel method: String, + havingResult result: @escaping FlutterResult, + withData data: [String: Any] + ) { + let resultData = Self.serialize(data: data) + MoEngagePluginCardsLogger.debug( + "Providing data \(data) to client for channel method \(method)", + forData: data + ) + DispatchQueue.main.async { result(resultData) } + } + + static func serialize(data: [String: Any]) -> String { + if let jsonData = try? JSONSerialization.data(withJSONObject: data), + let jsonStr = String(data: jsonData, encoding: .utf8) { + return jsonStr + } else { + return "" + } + } +} diff --git a/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageFlutterCardsConstants.swift b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageFlutterCardsConstants.swift new file mode 100644 index 00000000..1d9fe5b8 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/ios/Classes/MoEngageFlutterCardsConstants.swift @@ -0,0 +1,37 @@ +// +// MoEngageFlutterCardsConstants.swift +// moengage_cards +// +// Created by Soumya Mahunt on 22/06/23. +// + +import Foundation + +enum MoEngageFlutterCardsConstants { + static let pluginChannelName = "com.moengage/cards" + + enum FlutterToNativeMethods { + static let initialize = "initialize" + static let refreshCards = "refreshCards" + static let fetchCards = "fetchCards" + static let onCardSectionLoaded = "onCardSectionLoaded" + static let setAppOpenCardsSyncListener = "setAppOpenCardsSyncListener" + static let onCardSectionUnloaded = "onCardSectionUnLoaded" + static let getCardsCategories = "getCardsCategories" + static let cardsInfo = "getCardsInfo" + static let cardClicked = "cardClicked" + static let cardDelivered = "cardDelivered" + static let cardShown = "cardShown" + static let cardsForCategory = "cardsForCategory" + static let deleteCards = "deleteCards" + static let isAllCategoryEnabled = "isAllCategoryEnabled" + static let newCardsCount = "getNewCardsCount" + static let unClickedCardsCount = "unClickedCardsCount" + } + + enum NativeToFlutterMethods { + static let inboxOpenCardsSync = "onInboxOpenCardsSync" + static let pullToRefreshCardsSync = "onPullToRefreshCardsSync" + static let appOpenCardsSync = "onAppOpenCardsSync" + } +} diff --git a/flutter-sample/cards/moengage_cards_ios/ios/moengage_cards_ios.podspec b/flutter-sample/cards/moengage_cards_ios/ios/moengage_cards_ios.podspec new file mode 100644 index 00000000..5a9d61b0 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/ios/moengage_cards_ios.podspec @@ -0,0 +1,25 @@ +require 'yaml' +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +libraryVersion = pubspec['version'].gsub('+', '-') + +Pod::Spec.new do |s| + s.name = 'moengage_cards_ios' + s.version = libraryVersion + s.summary = 'A flutter plugin for using Cards from MoEngage iOS SDKs.' + s.description = <<-DESC + A flutter plugin for using Cards from MoEngage iOS SDKs. + DESC + s.homepage = 'https://www.moengage.com/' + s.license = { :file => '../LICENSE' } + s.author = { 'MoEngage Inc.' => 'mobiledevs@moengage.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.platform = :ios, '11.0' + + s.dependency 'Flutter' + s.dependency 'MoEngagePluginCards', '~> 1.2.1' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/flutter-sample/cards/moengage_cards_ios/lib/moengage_cards_ios.dart b/flutter-sample/cards/moengage_cards_ios/lib/moengage_cards_ios.dart new file mode 100644 index 00000000..8835497a --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/lib/moengage_cards_ios.dart @@ -0,0 +1,144 @@ +import 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' + show Logger, getAccountMeta; + +/// iOS specific implementation of Cards platform interface +class MoEngageCardsIOS extends MoEngageCardsPlatform { + /// Registers this class as the default instance of [MoEngageCardsPlatformInterface] + static void registerWith() { + Logger.v('Registering MoEngageCardsIOS with Platform Interface'); + MoEngageCardsPlatformInterface.instance = MoEngageCardsIOS(); + } + + @override + void initialize(String appId) { + methodChannel.invokeMethod(methodInitialize, getAccountMeta(appId)); + } + + @override + void refreshCards(String appId, CardsSyncListener cardsSyncListener) { + super.refreshCards(appId, cardsSyncListener); + methodChannel.invokeMethod(methodRefreshCards, getAccountMeta(appId)); + } + + @override + Future fetchCards(String appId) async { + final result = await methodChannel.invokeMethod( + methodFetchCards, + getAccountMeta(appId), + ); + return deSerializeCardsData(result as String); + } + + @override + void onCardsSectionLoaded(String appId, CardsSyncListener cardsSyncListener) { + super.onCardsSectionLoaded(appId, cardsSyncListener); + methodChannel.invokeMethod( + methodOnCardSectionLoaded, + getAccountMeta(appId), + ); + } + + @override + void setAppOpenCardsSyncListener( + CardsSyncListener cardsSyncListener, + String appId, + ) { + super.setAppOpenCardsSyncListener(cardsSyncListener, appId); + methodChannel.invokeMethod( + methodSetAppOpenCardsSyncListener, + getAccountMeta(appId), + ); + } + + @override + void onCardsSectionUnLoaded(String appId) { + methodChannel.invokeMethod( + methodOnCardSectionUnLoaded, + getAccountMeta(appId), + ); + } + + @override + Future> getCardsCategories(String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsCategories, + getAccountMeta(appId), + ); + return deSerializeCardsCategories(result.toString()); + } + + @override + Future getCardsInfo(String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsInfo, + getAccountMeta(appId), + ); + return deSerializeCardsInfo(result.toString()); + } + + @override + void cardClicked(Card card, int widgetId, String appId) { + methodChannel.invokeMethod( + methodCardClicked, + getCardClickPayload(card, widgetId, appId), + ); + } + + @override + void cardDelivered(String appId) { + methodChannel.invokeMethod(methodCardDelivered, getAccountMeta(appId)); + } + + @override + void cardShown(Card card, String appId) { + methodChannel.invokeMethod( + methodCardShown, + getCardShownPayload(card, appId), + ); + } + + @override + Future getCardsForCategory(String category, String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsForCategory, + getCardsForCategoryPayload(category, appId), + ); + return deSerializeCardsData(result.toString()); + } + + @override + Future deleteCards(List cards, String appId) async { + await methodChannel.invokeMethod( + methodDeleteCards, + getDeleteCardsPayload(cards, appId), + ); + } + + @override + Future isAllCategoryEnabled(String appId) async { + final result = await methodChannel.invokeMethod( + methodIsAllCategoryEnabled, + getAccountMeta(appId), + ); + return deSerializeIsAllCategoryEnabled(result.toString()); + } + + @override + Future getNewCardsCount(String appId) async { + final result = await methodChannel.invokeMethod( + methodNewCardsCount, + getAccountMeta(appId), + ); + return deSerializeNewCardsCount(result.toString()); + } + + @override + Future getUnClickedCardsCount(String appId) async { + final result = await methodChannel.invokeMethod( + methodUnClickedCardsCount, + getAccountMeta(appId), + ); + return deSerializeUnClickedCardsCount(result.toString()); + } +} diff --git a/flutter-sample/cards/moengage_cards_ios/pubspec.yaml b/flutter-sample/cards/moengage_cards_ios/pubspec.yaml new file mode 100644 index 00000000..bafd737b --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/pubspec.yaml @@ -0,0 +1,28 @@ +name: moengage_cards_ios +description: iOS implementation of the moengage_cards plugin +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_cards + platforms: + ios: + pluginClass: MoEngageCardsPlugin + dartPluginClass: MoEngageCardsIOS + +dependencies: + flutter: + sdk: flutter + moengage_cards_platform_interface: ^1.0.0 + moengage_flutter: ^6.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/cards/moengage_cards_ios/pubspec_overrides.yaml b/flutter-sample/cards/moengage_cards_ios/pubspec_overrides.yaml new file mode 100644 index 00000000..05bb2fe3 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/pubspec_overrides.yaml @@ -0,0 +1,13 @@ +dependency_overrides: + moengage_cards_platform_interface: + path: ../moengage_cards_platform_interface/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_ios/test/moengage_cards_ios_test.dart b/flutter-sample/cards/moengage_cards_ios/test/moengage_cards_ios_test.dart new file mode 100644 index 00000000..1f508431 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_ios/test/moengage_cards_ios_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/CHANGELOG.md b/flutter-sample/cards/moengage_cards_platform_interface/CHANGELOG.md new file mode 100644 index 00000000..a9ca1276 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/CHANGELOG.md @@ -0,0 +1,11 @@ +# MoEngage Cards Platform Interface + +# 07-12-2023 + +## 1.1.0 +- Updated Minimum Supported `moengage_flutter` version to `6.1.0` + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_platform_interface/LICENSE b/flutter-sample/cards/moengage_cards_platform_interface/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_platform_interface/README.md b/flutter-sample/cards/moengage_cards_platform_interface/README.md new file mode 100644 index 00000000..0b84e185 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/README.md @@ -0,0 +1,26 @@ +# moengage_cards_platform_interface + +A common platform interface for the [`moengage_cards`][1] plugin. + +This interface allows platform-specific implementations of the `moengage_cards` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `moengage_cards`, extend +[`MoEngageCardsPlatformInterface`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`MoEngageCardsPlatformInterface` by calling +`MoEngageCardsPlatformInterface.instance = MyPlatformMoEngageCards()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../moengage_cards_platform_interface +[2]: lib/moengage_cards_platform_interface.dart \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/moengage_cards_platform_interface.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/moengage_cards_platform_interface.dart new file mode 100644 index 00000000..eb5b5233 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/moengage_cards_platform_interface.dart @@ -0,0 +1,127 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'moengage_cards_platform_interface.dart'; +import 'src/internal/method_channel_moengage_cards.dart'; + +export 'src/internal/cards_controller.dart'; +export 'src/internal/cards_platform.dart'; +export 'src/internal/constants.dart'; +export 'src/internal/payload_mapper.dart'; +export 'src/model/model.dart'; + +/// Card Sync Listener +typedef CardsSyncListener = void Function(SyncCompleteData? data); + +/// Platform Interface for Cards Plugin +abstract class MoEngageCardsPlatformInterface extends PlatformInterface { + /// Constructor for [MoEngageCardsPlatformInterface] + MoEngageCardsPlatformInterface() : super(token: _token); + + /// Platform Specific Implementation of MoEngage Cards Plugin + static MoEngageCardsPlatformInterface _instance = + MethodChannelMoEngageCards(); + + /// Token to validate Actual Implementation and Mock Implementation for test + static final Object _token = Object(); + + /// Instance of [MoEngageCardsPlatformInterface] + static MoEngageCardsPlatformInterface get instance => _instance; + + /// Self Registration of Platform Interface used for tests and Platform specific packages + /// [instance] - Platform Specific Implementation of [MoEngageCardsPlatformInterface] + static set instance(MoEngageCardsPlatformInterface instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Initialize the cards module + void initialize(String appId) => throw UnimplementedError(); + + /// Ask the SDK to refresh cards on user request + /// Note: This API is to be used to mimic the pull to refresh behaviour + /// [cardsSyncListener] - Callback for Card Sync Completion of type [CardsSyncListener] + void refreshCards(String appId, CardsSyncListener cardsSyncListener) => + throw UnimplementedError(); + + /// Returns all Cards data of type [CardsData] + /// Note: This API refreshes cards data if inbox sync time interval expired + /// [appId] - MoEngage App ID + Future fetchCards(String appId) async => + throw UnimplementedError(); + + /// Notify the MoEngage SDK that card section has loaded + /// Note: This API should be used when the cards/inbox screen is visible to user + /// [cardsSyncListener] - Callback for Card Sync Completion of type [CardsSyncListener] + /// [appId] - MoEngage App ID + void onCardsSectionLoaded( + String appId, CardsSyncListener cardsSyncListener) => + throw UnimplementedError(); + + /// Notifies the SDK that the inbox view is gone to the background + /// Note: This API should be used when the cards screen is invisible to user + /// [appId] - MoEngage App ID + void onCardsSectionUnLoaded(String appId) => throw UnimplementedError(); + + /// Returns a [List] of [String] categories to be shown + /// [appId] - MoEngage App ID + Future> getCardsCategories(String appId) async => + throw UnimplementedError(); + + /// Fetches all cards related data + /// [appId] - MoEngage App ID + Future getCardsInfo(String appId) async => + throw UnimplementedError(); + + /// Marks a card as clicked and tracks an event for statistical purpose + /// [card] - Instance of [Card] + /// [widgetId] - Id of Widget , on which click action is performed + /// [appId] - MoEngage App ID + void cardClicked(Card card, int widgetId, String appId) => + throw UnimplementedError(); + + /// Notify SDK that the card is delivered. + /// Used for statistical purpose + /// [appId] - MoEngage App ID + void cardDelivered(String appId) => throw UnimplementedError(); + + /// Notify SDK the card is shown to the user - Used for statistical purposes + /// It is recommended to call this API in initState in Card Stateful Widget + /// [card] - Instance of [Card] + /// [appId] - MoEngage App ID + void cardShown(Card card, String appId) => throw UnimplementedError(); + + /// Fetch Cards for given [category] + /// [appId] - MoEngage App ID + Future getCardsForCategory(String category, String appId) => + throw UnimplementedError(); + + /// Deletes multiple cards at same time. + /// [card] - List of [Card] object + /// [appId] - MoEngage App ID + Future deleteCards(List cards, String appId) async => + throw UnimplementedError(); + + /// Return true if All cards category should be shown + /// [appId] - MoEngage App ID + Future isAllCategoryEnabled(String appId) async => + throw UnimplementedError(); + + /// Return count of new cards available + /// [appId] - MoEngage App ID + Future getNewCardsCount(String appId) async => + throw UnimplementedError(); + + /// Return count of UnClicked cards + /// [appId] - MoEngage App ID + Future getUnClickedCardsCount(String appId) async => + throw UnimplementedError(); + + /// Listener for Cards App Open Sync Listener + /// [cardsSyncListener] - Callback for Card Sync Completion of type [CardsSyncListener] + /// [appId] - MoEngage App ID + void setAppOpenCardsSyncListener( + CardsSyncListener cardsSyncListener, + String appId, + ) => + throw UnimplementedError(); +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/callback_cache.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/callback_cache.dart new file mode 100644 index 00000000..cc8a217a --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/callback_cache.dart @@ -0,0 +1,13 @@ +import '../../moengage_cards_platform_interface.dart'; + +/// Callback Cache for Cards Module +class CallbackCache { + /// App Open Cards Sync Listener + CardsSyncListener? appOpenSyncListener; + + /// Inbox Open Cards Sync Listener + CardsSyncListener? inboxOpenSyncListener; + + /// Pull To Refresh Cards Sync Listener + CardsSyncListener? pullToRefreshOpenSyncListener; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_controller.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_controller.dart new file mode 100644 index 00000000..363de1c4 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_controller.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:moengage_flutter/moengage_flutter.dart'; +import '../../moengage_cards_platform_interface.dart'; +import 'cards_instance_provider.dart'; + +/// Cards Method Channel Controller +class CardsController { + /// Factory Constructor + factory CardsController.init() => _instance; + + CardsController._internal() { + _channel.setMethodCallHandler(_handler); + } + + final String _tag = '${moduleTag}CardsController'; + + static final CardsController _instance = CardsController._internal(); + + final MethodChannel _channel = const MethodChannel(cardsMethodChannel); + + Future _handler(MethodCall call) async { + try { + final arguments = call.arguments; + if (arguments == null) { + return; + } + final json = jsonDecode(arguments.toString()); + final AccountMeta accountMeta = + accountMetaFromMap(json[keyAccountMeta] as Map); + if (call.method == methodPullToRefreshCardsSync) { + final syncJson = json[keyData]?[keySyncCompleteData]; + final SyncCompleteData? data = (syncJson != null) + ? SyncCompleteData.fromJson(syncJson as Map) + : null; + CardsInstanceProvider() + .getCallbackCacheForInstance(accountMeta.appId) + .pullToRefreshOpenSyncListener + ?.call(data); + } else if (call.method == methodOnAppOpenCardsSync) { + final syncJson = json[keyData]?[keySyncCompleteData]; + final SyncCompleteData? data = (syncJson != null) + ? SyncCompleteData.fromJson(syncJson as Map) + : null; + CardsInstanceProvider() + .getCallbackCacheForInstance(accountMeta.appId) + .appOpenSyncListener + ?.call(data); + } else if (call.method == methodOnInboxOpenCardsSync) { + final syncJson = json?[keyData]?[keySyncCompleteData]; + final SyncCompleteData? data = (syncJson != null) + ? SyncCompleteData.fromJson(syncJson as Map) + : null; + CardsInstanceProvider() + .getCallbackCacheForInstance(accountMeta.appId) + .inboxOpenSyncListener + ?.call(data); + } + } catch (e, stackTrace) { + Logger.e( + '$_tag _handler(): Error: $call has an Exception:', + error: e, + stackTrace: stackTrace, + ); + } + } +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_instance_provider.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_instance_provider.dart new file mode 100644 index 00000000..1b76009c --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_instance_provider.dart @@ -0,0 +1,22 @@ +// ignore_for_file: public_member_api_docs +import 'callback_cache.dart'; + +class CardsInstanceProvider { + factory CardsInstanceProvider() => _instance; + CardsInstanceProvider._internal(); + static final CardsInstanceProvider _instance = + CardsInstanceProvider._internal(); + + final Map _caches = {}; + + CallbackCache getCallbackCacheForInstance(String appId) { + final CallbackCache? cache = _caches[appId]; + if (cache != null) { + return cache; + } else { + final CallbackCache instanceCache = CallbackCache(); + _caches[appId] = instanceCache; + return instanceCache; + } + } +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_platform.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_platform.dart new file mode 100644 index 00000000..104c8ac1 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/cards_platform.dart @@ -0,0 +1,33 @@ +import 'package:flutter/services.dart'; +import '../../moengage_cards_platform_interface.dart'; +import 'cards_instance_provider.dart'; + +/// Common Implementation of Cards Platform Interface +abstract class MoEngageCardsPlatform extends MoEngageCardsPlatformInterface { + /// Cards Method Channel + MethodChannel methodChannel = const MethodChannel(cardsMethodChannel); + + @override + void refreshCards(String appId, CardsSyncListener cardsSyncListener) { + CardsInstanceProvider() + .getCallbackCacheForInstance(appId) + .pullToRefreshOpenSyncListener = cardsSyncListener; + } + + @override + void onCardsSectionLoaded(String appId, CardsSyncListener cardsSyncListener) { + CardsInstanceProvider() + .getCallbackCacheForInstance(appId) + .inboxOpenSyncListener = cardsSyncListener; + } + + @override + void setAppOpenCardsSyncListener( + CardsSyncListener cardsSyncListener, + String appId, + ) { + CardsInstanceProvider() + .getCallbackCacheForInstance(appId) + .appOpenSyncListener = cardsSyncListener; + } +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/constants.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/constants.dart new file mode 100644 index 00000000..06544e9b --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/constants.dart @@ -0,0 +1,102 @@ +// ignore_for_file: public_member_api_docs +const String moduleTag = 'Cards_'; + +//Cards Method Channel +const String cardsMethodChannel = 'com.moengage/cards'; + +//Card Key Name Constants +const String keyUnClickedCount = 'unClickedCount'; +const String keySyncCompleteData = 'syncCompleteData'; +const String keyHasUpdates = 'hasUpdates'; +const String keySyncType = 'syncType'; +const String keyId = 'id'; +const String keyCardId = 'card_id'; +const String keyCategory = 'category'; +const String keyTemplateData = 'template_data'; +const String keyMetaData = 'meta_data'; +const String keyAdditionalMetaData = 'metaData'; +const String keyTemplateType = 'type'; +const String keyKVPairs = 'kvPairs'; + +//Template Constants +const String keyContainers = 'containers'; +const String keyContainerId = 'id'; +const String keyContainerType = 'type'; +const String keyContainerStyle = 'style'; +const String keyBackgroundColor = 'bgColor'; +const String keyActions = 'actions'; +const String keyWidgets = 'widgets'; +const String keyWidgetId = 'id'; +const String keyWidgetType = 'type'; +const String keyWidgetContent = 'content'; +const String keyWidgetStyle = 'style'; +const String keyFontSize = 'fontSize'; +const String keyActionType = 'name'; +const String keyActionValue = 'value'; +const String keyNavigationType = 'type'; + +//Default Values +const int defaultFontSize = -1; +const String defaultTextBgColor = ''; +const String defaultContainerBgColor = ''; + +//CampaignState +const String keyLocalShowCount = 'localShowCount'; +const String keyTotalShowCount = 'totalShowCount'; +const String keyIsClicked = 'isClicked'; +const String keyFirstSeen = 'firstSeen'; +const String keyFirstReceived = 'firstReceived'; +const String keyIsNewCard = 'isNewCard'; +const String keyCampaignPayload = 'campaignPayload'; +const String keyCampaignState = 'campaignState'; +const String keyDeletionTime = 'deletionTime'; +const String keyUpdatedAt = 'updated_at'; +const String keyCreatedAt = 'created_at'; + +//Display Control Constants +const String keyExpireAt = 'expire_at'; +const String keyExpireAfterSeen = 'expire_after_seen'; +const String keyExpireAfterDelivered = 'expire_after_delivered'; +const String keyMaxCount = 'max_times_to_show'; +const String keyShowTime = 'show_time'; +const String keyIsPinned = 'is_pin'; +const String keyStartTime = 'start_time'; +const String keyEndTime = 'end_time'; + +//Card Data Constants +const String keyDisplayControl = 'display_controls'; +const String keyShouldShowAllTab = 'shouldShowAllTab'; +const String keyCategories = 'categories'; +const String keyCards = 'cards'; +const String keyCard = 'card'; +const String keyWidgetIdentifier = 'widgetId'; +const String keyIsAllCategoryEnabled = 'isAllCategoryEnabled'; +const String keyNewCardsCount = 'newCardsCount'; +const String keyUnClickedCardsCount = 'unClickedCardsCount'; + +// Platform Channel Methods +const String methodInitialize = 'initialize'; +const String methodRefreshCards = 'refreshCards'; +const String methodFetchCards = 'fetchCards'; +const String methodOnCardSectionLoaded = 'onCardSectionLoaded'; +const String methodSetAppOpenCardsSyncListener = 'setAppOpenCardsSyncListener'; +const String methodOnCardSectionUnLoaded = 'onCardSectionUnLoaded'; +const String methodCardsCategories = 'getCardsCategories'; +const String methodCardsInfo = 'getCardsInfo'; +const String methodCardClicked = 'cardClicked'; +const String methodCardDelivered = 'cardDelivered'; +const String methodCardShown = 'cardShown'; +const String methodCardsForCategory = 'cardsForCategory'; +const String methodDeleteCards = 'deleteCards'; +const String methodIsAllCategoryEnabled = 'isAllCategoryEnabled'; +const String methodNewCardsCount = 'getNewCardsCount'; +const String methodUnClickedCardsCount = 'unClickedCardsCount'; +const String methodOnInboxOpenCardsSync = 'onInboxOpenCardsSync'; +const String methodPullToRefreshCardsSync = 'onPullToRefreshCardsSync'; +const String methodOnAppOpenCardsSync = 'onAppOpenCardsSync'; + +//JSON Values Constants +const String argumentPullToRefreshSync = 'PULL_TO_REFRESH'; +const String argumentInboxOpenSync = 'INBOX_OPEN'; +const String argumentAppOpenSync = 'APP_OPEN'; +const String argumentAllCards = 'All'; diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/method_channel_moengage_cards.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/method_channel_moengage_cards.dart new file mode 100644 index 00000000..e9393c36 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/method_channel_moengage_cards.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; + +import 'package:moengage_flutter/moengage_flutter.dart' show getAccountMeta; + +import '../../moengage_cards_platform_interface.dart'; + +/// An implementation of [MoEngageCardsPlatform] that uses method channels. +class MethodChannelMoEngageCards extends MoEngageCardsPlatform { + @override + void initialize(String appId) { + methodChannel.invokeMethod( + methodInitialize, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + void refreshCards(String appId, CardsSyncListener cardsSyncListener) { + super.refreshCards(appId, cardsSyncListener); + methodChannel.invokeMethod( + methodRefreshCards, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + Future fetchCards(String appId) async { + final result = await methodChannel.invokeMethod( + methodFetchCards, + getAccountMeta(appId), + ); + return deSerializeCardsData(result as String); + } + + @override + void onCardsSectionLoaded(String appId, CardsSyncListener cardsSyncListener) { + super.onCardsSectionLoaded(appId, cardsSyncListener); + methodChannel.invokeMethod( + methodOnCardSectionLoaded, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + void onCardsSectionUnLoaded(String appId) { + methodChannel.invokeMethod( + methodOnCardSectionUnLoaded, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + Future> getCardsCategories(String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsCategories, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeCardsCategories(result as String); + } + + @override + Future getCardsInfo(String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsInfo, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeCardsInfo(result as String); + } + + @override + void cardClicked(Card card, int widgetId, String appId) { + methodChannel.invokeMethod( + methodCardClicked, + jsonEncode(getCardClickPayload(card, widgetId, appId)), + ); + } + + @override + void cardDelivered(String appId) { + methodChannel.invokeMethod( + methodCardDelivered, + jsonEncode(getAccountMeta(appId)), + ); + } + + @override + void cardShown(Card card, String appId) { + methodChannel.invokeMethod( + methodCardShown, + jsonEncode(getCardShownPayload(card, appId)), + ); + } + + @override + Future getCardsForCategory(String category, String appId) async { + final result = await methodChannel.invokeMethod( + methodCardsForCategory, + jsonEncode(getCardsForCategoryPayload(category, appId)), + ); + return deSerializeCardsData(result as String); + } + + @override + Future deleteCards(List cards, String appId) async { + await methodChannel.invokeMethod( + methodDeleteCards, + jsonEncode(getDeleteCardsPayload(cards, appId)), + ); + } + + @override + Future isAllCategoryEnabled(String appId) async { + final result = await methodChannel.invokeMethod( + methodIsAllCategoryEnabled, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeIsAllCategoryEnabled(result as String); + } + + @override + Future getNewCardsCount(String appId) async { + final result = await methodChannel.invokeMethod( + methodNewCardsCount, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeNewCardsCount(result as String); + } + + @override + Future getUnClickedCardsCount(String appId) async { + final result = await methodChannel.invokeMethod( + methodUnClickedCardsCount, + jsonEncode(getAccountMeta(appId)), + ); + return deSerializeUnClickedCardsCount(result as String); + } +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/payload_mapper.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/payload_mapper.dart new file mode 100644 index 00000000..fcb420a1 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/internal/payload_mapper.dart @@ -0,0 +1,133 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:convert'; + +import 'package:moengage_flutter/moengage_flutter.dart' + show keyAppId, keyAccountMeta, keyData; + +import '../../moengage_cards_platform_interface.dart'; + +WidgetStyle? widgetStyleFromJson( + Map? json, + WidgetType widgetType, +) { + if (json == null) { + return null; + } + switch (widgetType) { + case WidgetType.text: + return TextStyle.fromJson(json); + case WidgetType.button: + return ButtonStyle.fromJson(json); + case WidgetType.image: + return ImageStyle.fromJson(json); + } +} + +Action actionStyleFromJson(Map json) { + final ActionType actionType = + ActionType.values.byName(json[keyActionType] as String); + switch (actionType) { + case ActionType.navigate: + return NavigationAction.fromJson(json); + } +} + +List deSerializeCardsCategories(String payload) { + final List categories = []; + final Map data = + json.decode(payload)[keyData] as Map; + final Iterable cardCategories = + (data[keyCategories] ?? []) as Iterable; + for (final data in cardCategories) { + categories.add(data.toString()); + } + return categories; +} + +CardsInfo deSerializeCardsInfo(String payload) { + final dataPayload = jsonDecode(payload)[keyData]; + return CardsInfo.fromJson(dataPayload as Map); +} + +CardsData deSerializeCardsData(String payload) { + final dataPayload = jsonDecode(payload)[keyData]; + return CardsData.fromJson(dataPayload as Map); +} + +bool deSerializeIsAllCategoryEnabled(String payload) { + final Map dataPayload = + jsonDecode(payload)[keyData] as Map; + return (dataPayload[keyIsAllCategoryEnabled] ?? false) as bool; +} + +int deSerializeNewCardsCount(String payload) { + final Map dataPayload = + jsonDecode(payload)[keyData] as Map; + return (dataPayload[keyNewCardsCount] ?? 0) as int; +} + +int deSerializeUnClickedCardsCount(String payload) { + final Map json = jsonDecode(payload) as Map; + final Map dataPayload = + json[keyData] as Map; + return (dataPayload[keyUnClickedCardsCount] ?? 0) as int; +} + +Map getCardClickPayload( + Card card, + int widgetId, + String appId, +) { + return { + keyAccountMeta: {keyAppId: appId}, + keyData: {keyWidgetIdentifier: widgetId, keyCard: card.toJson()} + }; +} + +Map getCardShownPayload(Card card, String appId) { + return { + keyAccountMeta: getAppIdPayload(appId), + keyData: {keyCard: card.toJson()} + }; +} + +Map getCardsForCategoryPayload(String category, String appId) { + return { + keyAccountMeta: getAppIdPayload(appId), + keyData: {keyCategory: category} + }; +} + +Map getDeleteCardsPayload(List cards, String appId) { + return { + keyAccountMeta: getAppIdPayload(appId), + keyData: {keyCards: cards.map((Card e) => e.toJson()).toList()} + }; +} + +SyncType syncTypeFromString(String? syncType) { + switch (syncType) { + case argumentPullToRefreshSync: + return SyncType.pullToRefresh; + case argumentInboxOpenSync: + return SyncType.inboxOpen; + case argumentAppOpenSync: + return SyncType.appOpen; + default: + throw UnimplementedError('Sync Type Not Supported'); + } +} + +String syncTypeToString(SyncType syncType) { + switch (syncType) { + case SyncType.pullToRefresh: + return argumentPullToRefreshSync; + case SyncType.inboxOpen: + return argumentInboxOpenSync; + case SyncType.appOpen: + return argumentAppOpenSync; + } +} + +Map getAppIdPayload(String appId) => {keyAppId: appId}; diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/action/action.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/action/action.dart new file mode 100644 index 00000000..94864542 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/action/action.dart @@ -0,0 +1,13 @@ +import '../enums/action_type.dart'; + +///Base class all Action. +abstract class Action { + /// [Action] constructor + Action(this.actionType); + + /// Action Type - Currently Only Navigation Action is Supported + ActionType actionType; + + /// Abstract Function, should be overridden by Implementation Class + Map toJson(); +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/action/navigation_action.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/action/navigation_action.dart new file mode 100644 index 00000000..0e1d4e09 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/action/navigation_action.dart @@ -0,0 +1,51 @@ +import '../../internal/constants.dart'; +import '../enums/action_type.dart'; +import '../navigation_type.dart'; +import 'action.dart'; + +/// Navigation Action Data +class NavigationAction extends Action { + /// [NavigationAction] constructor + NavigationAction({ + required ActionType actionType, + required this.navigationType, + required this.value, + required this.keyValuePairs, + }) : super(actionType); + + /// Get [NavigationAction] from Json [Map] + factory NavigationAction.fromJson(Map json) { + return NavigationAction( + actionType: ActionType.navigate, + navigationType: + NavigationType.values.byName(json[keyNavigationType] as String), + value: (json[keyActionValue] ?? '') as String, + keyValuePairs: (json[keyKVPairs]) as Map, + ); + } + + /// Type of Navigation action. + /// + /// Possible value deepLinking or screenName or richLanding + NavigationType navigationType; + + /// Value can be Deeplink Url, RichLanding Url or Screen Name + String value; + + /// [Map] of Key-Value pairs entered on the MoEngage Platform for + /// navigation action of the campaign. + Map keyValuePairs; + + @override + String toString() { + return 'NavigationAction{navigationType: $navigationType, value: $value, keyValuePairs: $keyValuePairs}'; + } + + @override + Map toJson() => { + keyActionType: actionType.name, + keyNavigationType: navigationType.name, + keyActionValue: value, + keyKVPairs: keyValuePairs + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/campaign_state.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/campaign_state.dart new file mode 100644 index 00000000..ad0641a3 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/campaign_state.dart @@ -0,0 +1,48 @@ +import '../internal/constants.dart'; + +/// State of the card Campaign +class CampaignState { + /// [CampaignState] Constructor + CampaignState({ + required this.localShowCount, + required this.isClicked, + required this.firstReceived, + required this.firstSeen, + required this.totalShowCount, + }); + + /// Get [CampaignState] from Json [Map] + factory CampaignState.fromJson(Map json) { + return CampaignState( + localShowCount: (json[keyLocalShowCount] ?? 0) as int, + isClicked: (json[keyIsClicked] ?? false) as bool, + firstReceived: (json[keyFirstReceived] ?? -1) as int, + firstSeen: (json[keyFirstSeen] ?? -1) as int, + totalShowCount: (json[keyTotalShowCount] ?? 0) as int, + ); + } + + /// Number of times card shown on the current device + final int localShowCount; + + /// True if the user has clicked the card, else false. + bool isClicked; + + /// First Time the card was received. + final int firstReceived; + + /// First Time the card was seen by the user. + final int firstSeen; + + /// Total number of times campaign has been seen by the user across devices. + final int totalShowCount; + + /// Convert [CampaignState] to Json [Map] + Map toJson() => { + keyLocalShowCount: localShowCount, + keyIsClicked: isClicked, + keyFirstReceived: firstReceived, + keyFirstSeen: firstSeen, + keyTotalShowCount: totalShowCount + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/card.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/card.dart new file mode 100644 index 00000000..4cf2c3b1 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/card.dart @@ -0,0 +1,57 @@ +import '../internal/constants.dart'; +import 'meta_data.dart' as moe; +import 'template.dart'; + +/// Card Campaign data +class Card { + /// [Card] Constructor + Card({ + required this.id, + required this.cardId, + required this.category, + required this.template, + required this.metaData, + }); + + /// Get [Card] from Json [Map] + factory Card.fromJson(Map json) { + return Card( + id: (json[keyId] ?? -1) as int, + cardId: (json[keyCardId]) as String, + category: (json[keyCategory] ?? '') as String, + template: + Template.fromJson(json[keyTemplateData] as Map), + metaData: + moe.MetaData.fromJson(json[keyMetaData] as Map), + ); + } + + /// Internal Identifier for Card. + int id; + + /// Unique identifier for the Card campaign + String cardId; + + /// Category to which the campaign belongs. + String category; + + /// Template payload for the campaign + Template template; + + /// Meta data related to the campaign like status, delivery control etc. + moe.MetaData metaData; + + /// Convert [Card] to Json [Map] + Map toJson() => { + keyId: id, + keyCardId: cardId, + keyCategory: category, + keyTemplateData: template.toJson(), + keyMetaData: metaData.toJson() + }; + + @override + String toString() { + return 'Card{id: $id, cardId: $cardId, category: $category, template: $template, metaData: $metaData}'; + } +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/cards_data.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/cards_data.dart new file mode 100644 index 00000000..cbd9e742 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/cards_data.dart @@ -0,0 +1,28 @@ +import '../internal/constants.dart'; +import 'card.dart'; + +/// Data for Cards for particular Category +class CardsData { + /// [CardsData] Constructor + CardsData({required this.category, required this.cards}); + + /// Get [CardsData] from Json [Map] + factory CardsData.fromJson(Map json) => CardsData( + category: (json[keyCategory] ?? argumentAllCards) as String, + cards: List.from((json[keyCards] ?? []) as Iterable) + .map((e) => Card.fromJson(e as Map)) + .toList(), + ); + + /// Category in which Cards belong to + String category; + + /// [List] of [Card] Model + List cards; + + /// Convert [CardsData] to Json [Map] + Map toJson() => { + keyCategory: category, + keyCards: cards.map((Card e) => e.toJson()).toList() + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/cards_info.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/cards_info.dart new file mode 100644 index 00000000..abf454e3 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/cards_info.dart @@ -0,0 +1,41 @@ +import '../internal/constants.dart'; +import 'card.dart'; + +/// All data for cards. +class CardsInfo { + /// [CardsInfo] Constructor + CardsInfo({ + required this.shouldShowAllTab, + required this.categories, + required this.cards, + }); + + /// Get [CardsInfo] from Json [Map] + factory CardsInfo.fromJson(Map json) { + return CardsInfo( + shouldShowAllTab: (json[keyShouldShowAllTab] ?? false) as bool, + categories: List.from((json[keyCategories] ?? []) as Iterable) + .map((e) => e.toString()) + .toList(), + cards: List.from((json[keyCards] ?? []) as Iterable) + .map((e) => Card.fromJson(e as Map)) + .toList(), + ); + } + + /// True is showing ALL tabs is enabled else false. + final bool shouldShowAllTab; + + /// All configured categories for cards. + final List categories; + + /// All cards which are eligible for display currently. + final List cards; + + /// Convert [CardsInfo] to Json [Map] + Map toJson() => { + keyShouldShowAllTab: shouldShowAllTab, + keyCategories: categories, + keyCards: cards.map((Card e) => e.toJson()) + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/container.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/container.dart new file mode 100644 index 00000000..57b3fc59 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/container.dart @@ -0,0 +1,62 @@ +import '../internal/constants.dart'; +import '../internal/payload_mapper.dart'; +import 'action/action.dart'; +import 'enums/template_type.dart'; +import 'style/container_style.dart'; +import 'widget.dart'; + +/// Container to hold UI widget. Equivalent to a Container Widget in Flutter +class Container { + /// [Container] Constructor + Container({ + required this.id, + required this.templateType, + required this.style, + required this.widgets, + required this.actionList, + }); + + /// Get [Container] from Json [Map] + factory Container.fromJson(Map json) { + return Container( + id: (json[keyContainerId]) as int, + templateType: + TemplateType.values.byName(json[keyContainerType] as String), + widgets: ((json[keyWidgets] ?? []) as List) + .map((e) => Widget.fromJson(e as Map)) + .toList(), + actionList: ((json[keyActions] ?? []) as List) + .map((e) => actionStyleFromJson(e as Map)) + .toList(), + style: (json[keyContainerStyle] != null) + ? ContainerStyle.fromJson( + (json[keyContainerStyle]) as Map, + ) + : null, + ); + } + + /// Unique identifier for a template + final int id; + + /// Type of container + final TemplateType templateType; + + /// Style associated to the Container + final ContainerStyle? style; + + /// [List] of [Widget] + final List widgets; + + /// [List] of [Action] for the container + final List actionList; + + /// Convert [Container] to Json [Map] + Map toJson() => { + keyContainerId: id, + keyTemplateType: templateType.name, + keyContainerStyle: style?.toJson(), + keyWidgets: widgets.map((Widget e) => e.toJson()).toList(), + keyActions: actionList.map((Action e) => e.toJson()).toList() + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/display_control.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/display_control.dart new file mode 100644 index 00000000..12a35436 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/display_control.dart @@ -0,0 +1,58 @@ +import '../internal/constants.dart'; +import 'show_time.dart'; + +/// Delivery Controls defined during campaign creation. +class DisplayControl { + /// [DisplayControl] Constructor + DisplayControl({ + required this.expireAt, + required this.expireAfterSeen, + required this.expireAfterDelivered, + required this.maxCount, + required this.isPinned, + required this.showTime, + }); + + /// Get [DisplayControl] from Json [Map] + factory DisplayControl.fromJson(Map json) { + return DisplayControl( + expireAt: (json[keyExpireAt] ?? -1) as int, + expireAfterSeen: (json[keyExpireAfterSeen] ?? -1) as int, + expireAfterDelivered: (json[keyExpireAfterDelivered] ?? -1) as int, + maxCount: (json[keyMaxCount] ?? 0) as int, + isPinned: (json[keyIsPinned] ?? false) as bool, + showTime: ShowTime.fromJson( + (json[keyShowTime] ?? {}) as Map, + ), + ); + } + + /// Absolute time at which the card should be expired. + /// Value in seconds. + final int expireAt; + + /// Time duration after which card should be expired once it is seen. + final int expireAfterSeen; + + /// Time duration after which the card should be expired once it is delivered on the device. + final int expireAfterDelivered; + + /// Maximum number of times a campaign should be shown to the user across devices. + final int maxCount; + + /// True if the campaign is pinned on top, else false. + final bool isPinned; + + /// Time during the day when the campaign should be shown. + final ShowTime showTime; + + /// Convert [DisplayControl] to Json [Map] + Map toJson() => { + keyExpireAt: expireAt, + keyExpireAfterSeen: expireAfterSeen, + keyExpireAfterDelivered: expireAfterDelivered, + keyMaxCount: maxCount, + keyIsPinned: isPinned, + keyShowTime: showTime.toJson() + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/action_type.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/action_type.dart new file mode 100644 index 00000000..cc9cab29 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/action_type.dart @@ -0,0 +1,6 @@ +/// Type of Action to be performed +/// Note: Currently, only Navigation Action is Supported +enum ActionType { + /// Navigation Action + navigate +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/template_type.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/template_type.dart new file mode 100644 index 00000000..80c50987 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/template_type.dart @@ -0,0 +1,8 @@ +/// Card Template Types available in Dashboard +enum TemplateType { + /// Basic Card Template + basic, + + /// Illustration Card Template + illustration +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/widget_type.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/widget_type.dart new file mode 100644 index 00000000..bb7f85af --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/enums/widget_type.dart @@ -0,0 +1,11 @@ +/// Types of UI widgets supported. +enum WidgetType { + /// Widget that loads text content. + text, + + /// Widget that loads an image or gif + image, + + /// Widget that loads button content. + button, +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/meta_data.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/meta_data.dart new file mode 100644 index 00000000..f9fbfd53 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/meta_data.dart @@ -0,0 +1,81 @@ +import '../internal/constants.dart'; +import 'campaign_state.dart'; +import 'display_control.dart'; + +/// Meta data related to a campaign. +class MetaData { + /// [MetaData] Constructor + MetaData({ + required this.isNewCard, + required this.campaignState, + required this.deletionTime, + required this.displayControl, + required this.metaData, + required this.updatedTime, + required this.createdAt, + required this.campaignPayload, + required this.isPinned, + }); + + /// Get [MetaData] from Json [Map] + factory MetaData.fromJson(Map json) { + return MetaData( + isNewCard: (json[keyIsNewCard] ?? false) as bool, + campaignState: CampaignState.fromJson( + (json[keyCampaignState] ?? {}) as Map, + ), + deletionTime: (json[keyDeletionTime] ?? -1) as int, + displayControl: DisplayControl.fromJson( + (json[keyDisplayControl] ?? {}) + as Map, + ), + campaignPayload: (json[keyCampaignPayload] ?? {}) + as Map, + metaData: (json[keyAdditionalMetaData] ?? {}) + as Map, + updatedTime: (json[keyUpdatedAt] ?? -1) as int, + createdAt: (json[keyCreatedAt] ?? -1) as int, + isPinned: (json[keyDisplayControl]?[keyIsPinned] ?? false) as bool, + ); + } + + /// True if the campaign hasn't been delivered to the inbox, else false. + final bool isNewCard; + + /// Current state of the campaign. + final CampaignState campaignState; + + /// Time at which the campaign would be deleted from local store. + final int deletionTime; + + /// Delivery Controls defined during campaign creation. + final DisplayControl displayControl; + + /// Additional meta data regarding campaign used for tracking purposes. + final Map metaData; + + /// Last time the campaign was updated. + final int updatedTime; + + /// Campaign Created Time + /// Note: Only Available for iOS platform + final int createdAt; + + /// Complete Campaign payload + final Map campaignPayload; + + /// Check Card is Pinned + final bool isPinned; + + /// Convert [MetaData] to Json [Map] + Map toJson() => { + keyIsNewCard: isNewCard, + keyCampaignState: campaignState.toJson(), + keyDeletionTime: deletionTime, + keyDisplayControl: displayControl.toJson(), + keyAdditionalMetaData: metaData, + keyUpdatedAt: updatedTime, + keyCreatedAt: createdAt, + keyCampaignPayload: campaignPayload + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/model.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/model.dart new file mode 100644 index 00000000..88002c73 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/model.dart @@ -0,0 +1,23 @@ +export 'action/action.dart'; +export 'action/navigation_action.dart'; +export 'campaign_state.dart'; +export 'card.dart'; +export 'cards_data.dart'; +export 'cards_info.dart'; +export 'container.dart'; +export 'display_control.dart'; +export 'enums/action_type.dart'; +export 'enums/template_type.dart'; +export 'enums/widget_type.dart'; +export 'meta_data.dart'; +export 'navigation_type.dart'; +export 'show_time.dart'; +export 'style/button_style.dart'; +export 'style/container_style.dart'; +export 'style/image_style.dart'; +export 'style/text_style.dart'; +export 'style/widget_style.dart'; +export 'sync_data.dart'; +export 'sync_type.dart'; +export 'template.dart'; +export 'widget.dart'; diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/navigation_type.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/navigation_type.dart new file mode 100644 index 00000000..46582542 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/navigation_type.dart @@ -0,0 +1,11 @@ +/// Navigation Action Types +enum NavigationType { + /// Navigation is done using screen name. + screenName, + + /// Navigation is done using a deep-link Url or http(s) url. + deepLink, + + /// Navigation to a rich-landing url + richLanding +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/show_time.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/show_time.dart new file mode 100644 index 00000000..be419a0d --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/show_time.dart @@ -0,0 +1,25 @@ +import '../internal/constants.dart'; + +/// Time span during card can be shown +class ShowTime { + /// [ShowTime] Constructor + ShowTime({required this.startTime, required this.endTime}); + + /// Get [ShowTime] from Json [Map] + factory ShowTime.fromJson(Map json) { + return ShowTime( + startTime: (json[keyStartTime] ?? '') as String, + endTime: (json[keyEndTime] ?? '') as String, + ); + } + + /// Start time for the time range. + String startTime; + + /// End time for the time range. + String endTime; + + /// Convert [ShowTime] to Json [Map] + Map toJson() => + {keyStartTime: startTime, keyEndTime: endTime}; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/button_style.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/button_style.dart new file mode 100644 index 00000000..7d1d8f5d --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/button_style.dart @@ -0,0 +1,25 @@ +import '../../internal/constants.dart'; +import 'widget_style.dart'; + +///Style for Button Widget - [WidgetType.button] +class ButtonStyle extends WidgetStyle { + /// [ButtonStyle] Constructor + ButtonStyle({required String backgroundColor, required this.fontSize}) + : super(backgroundColor); + + /// Get [ButtonStyle] from Json [Map] + factory ButtonStyle.fromJson(Map json) { + return ButtonStyle( + backgroundColor: + (json[keyBackgroundColor] ?? defaultTextBgColor) as String, + fontSize: (json[keyFontSize] ?? defaultFontSize) as int, + ); + } + + /// Font Size for Button Text + int fontSize; + + @override + Map toJson() => + {keyBackgroundColor: backgroundColor, keyFontSize: fontSize}; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/container_style.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/container_style.dart new file mode 100644 index 00000000..6d06e8d7 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/container_style.dart @@ -0,0 +1,21 @@ +import '../../internal/constants.dart'; + +/// Style for [Container] widget +class ContainerStyle { + /// [ContainerStyle] Constructor + ContainerStyle({required this.backgroundColor}); + + /// Get [ContainerStyle] from Json [Map] + factory ContainerStyle.fromJson(Map json) { + return ContainerStyle( + backgroundColor: + (json[keyBackgroundColor] ?? defaultContainerBgColor) as String, + ); + } + + /// Container Background Color Hex Code + String backgroundColor; + + /// Convert [ContainerStyle] to Json [Map] + Map toJson() => {keyBackgroundColor: backgroundColor}; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/image_style.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/image_style.dart new file mode 100644 index 00000000..ae6956ed --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/image_style.dart @@ -0,0 +1,19 @@ +import '../../internal/constants.dart'; +import 'widget_style.dart'; + +///Style for Button Widget - [WidgetType.image] +class ImageStyle extends WidgetStyle { + /// [ImageStyle] Constructor + ImageStyle({required String backgroundColor}) : super(backgroundColor); + + /// Get [ImageStyle] from Json [Map] + factory ImageStyle.fromJson(Map json) { + return ImageStyle( + backgroundColor: + (json[keyBackgroundColor] ?? defaultTextBgColor) as String, + ); + } + + @override + Map toJson() => {keyBackgroundColor: backgroundColor}; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/text_style.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/text_style.dart new file mode 100644 index 00000000..c068c3fa --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/text_style.dart @@ -0,0 +1,25 @@ +import '../../internal/constants.dart'; +import 'widget_style.dart'; + +///Style for Button Widget - [WidgetType.text] +class TextStyle extends WidgetStyle { + /// [TextStyle] Constructor + TextStyle({required String backgroundColor, required this.fontSize}) + : super(backgroundColor); + + /// Get [TextStyle] from Json [Map] + factory TextStyle.fromJson(Map json) { + return TextStyle( + backgroundColor: + (json[keyBackgroundColor] ?? defaultTextBgColor) as String, + fontSize: (json[keyFontSize] ?? defaultFontSize) as int, + ); + } + + ///Font Size for Text Widget + int fontSize; + + @override + Map toJson() => + {keyBackgroundColor: backgroundColor, keyFontSize: fontSize}; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/widget_style.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/widget_style.dart new file mode 100644 index 00000000..96ace45b --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/style/widget_style.dart @@ -0,0 +1,11 @@ +/// Base style for all widgets. +abstract class WidgetStyle { + /// [WidgetStyle] Constructor + WidgetStyle(this.backgroundColor); + + /// Background color for the widget. + String backgroundColor; + + /// Convert [WidgetStyle] to Json [Map] + Map toJson(); +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/sync_data.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/sync_data.dart new file mode 100644 index 00000000..82bba24f --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/sync_data.dart @@ -0,0 +1,33 @@ +import '../internal/constants.dart'; +import '../internal/payload_mapper.dart'; +import 'sync_type.dart'; + +/// Sync Complete Data +class SyncCompleteData { + /// [SyncCompleteData] Constructor + SyncCompleteData({required this.hasUpdates, required this.syncType}); + + /// Get [SyncCompleteData] from Json [Map] + factory SyncCompleteData.fromJson(Map data) { + return SyncCompleteData( + hasUpdates: (data[keyHasUpdates] ?? false) as bool, + syncType: syncTypeFromString(data[keySyncType] as String), + ); + } + + /// Indicating if there were any updates in the cards post sync. true if there are any new + /// updates present else false. This value is true even if card(s) are deleted. + bool hasUpdates; + + /// Condition under which sync was triggered. Refer to [SyncType] + SyncType syncType; + + /// Convert [SyncCompleteData] to Json [Map] + Map toJson() => + {keySyncType: syncTypeToString(syncType), keyHasUpdates: hasUpdates}; + + @override + String toString() { + return 'SyncCompleteData{hasUpdates: $hasUpdates, syncType: $syncType}'; + } +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/sync_type.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/sync_type.dart new file mode 100644 index 00000000..d2fd5775 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/sync_type.dart @@ -0,0 +1,11 @@ +/// Card Sync Types +enum SyncType { + /// Sync when user Opens the App + appOpen, + + /// Sync when user lands on Inbox Screen + inboxOpen, + + /// Sync when user performs pull to refresh action + pullToRefresh +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/template.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/template.dart new file mode 100644 index 00000000..9833ca10 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/template.dart @@ -0,0 +1,43 @@ +import '../internal/constants.dart'; +import 'container.dart'; +import 'enums/template_type.dart'; + +/// Card Template data. +class Template { + /// [Template] Constructor + Template({ + required this.templateType, + required this.containers, + required this.kvPairs, + }); + + /// Get [Template] from Json [Map] + factory Template.fromJson(Map json) { + return Template( + templateType: TemplateType.values.byName(json[keyTemplateType] as String), + containers: List.from( + ((json[keyContainers] ?? []) as Iterable).map( + (dataJson) => Container.fromJson(dataJson as Map), + ), + ), + kvPairs: + (json[keyKVPairs] ?? {}) as Map, + ); + } + + /// Type of Template + TemplateType templateType; + + /// Containers in the template. + List containers; + + /// Additional data associated to the template + Map kvPairs; + + /// Convert [Template] to Json [Map] + Map toJson() => { + keyTemplateType: templateType.name, + keyContainers: containers.map((Container e) => e.toJson()).toList(), + keyKVPairs: kvPairs + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/widget.dart b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/widget.dart new file mode 100644 index 00000000..52187b0b --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/lib/src/model/widget.dart @@ -0,0 +1,61 @@ +import '../internal/constants.dart'; +import '../internal/payload_mapper.dart'; +import 'action/action.dart'; +import 'enums/widget_type.dart'; +import 'style/widget_style.dart'; + +/// UI element in a card. +class Widget { + /// [Widget] Constructor + Widget({ + required this.id, + required this.widgetType, + required this.content, + required this.style, + required this.actionList, + }); + + /// Get [Widget] from Json [Map] + factory Widget.fromJson(Map json) { + final WidgetType widgetType = + WidgetType.values.byName(json[keyWidgetType].toString().toLowerCase()); + return Widget( + id: (json[keyWidgetId] ?? -1) as int, + widgetType: widgetType, + content: (json[keyWidgetContent] ?? '') as String, + style: widgetStyleFromJson( + (json[keyWidgetStyle] ?? {}) as Map, + widgetType, + ), + actionList: (json[keyActions] as Iterable) + .map( + (action) => actionStyleFromJson(action as Map), + ) + .toList(), + ); + } + + /// Identifier for the widget. + int id; + + /// Type of widget + WidgetType widgetType; + + /// Content to be loaded in the widget. + String content; + + /// Style associated with the widget + WidgetStyle? style; + + /// Actions to be performed on widget click + List actionList; + + /// Convert [Widget] to Json [Map] + Map toJson() => { + keyWidgetId: id, + keyWidgetContent: content, + keyWidgetType: widgetType.name, + keyWidgetStyle: style?.toJson(), + keyActions: actionList.map((Action e) => e.toJson()).toList() + }; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/pubspec.yaml b/flutter-sample/cards/moengage_cards_platform_interface/pubspec.yaml new file mode 100644 index 00000000..ff6b449c --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/pubspec.yaml @@ -0,0 +1,19 @@ +name: moengage_cards_platform_interface +description: A common platform interface for the moengage_cards plugin. +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.1.0 + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + collection: ^1.16.0 + flutter_test: + sdk: flutter diff --git a/flutter-sample/cards/moengage_cards_platform_interface/pubspec_overrides.yaml b/flutter-sample/cards/moengage_cards_platform_interface/pubspec_overrides.yaml new file mode 100644 index 00000000..446c4bf1 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/pubspec_overrides.yaml @@ -0,0 +1,11 @@ +dependency_overrides: + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/cards/moengage_cards_platform_interface/test/cards_comparator.dart b/flutter-sample/cards/moengage_cards_platform_interface/test/cards_comparator.dart new file mode 100644 index 00000000..f63d9e20 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/test/cards_comparator.dart @@ -0,0 +1,218 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; + +class CardsComparator { + bool isCardEqual(Card card1, Card card2) { + return card1.id == card2.id && + card1.category == card1.category && + card1.cardId == card2.cardId && + isTemplateEqual(card1.template, card2.template) && + isMetaDataEqual(card1.metaData, card2.metaData); + } + + bool isTemplateEqual(Template template1, Template template2) { + return mapEquals(template1.kvPairs, template2.kvPairs) && + template1.templateType == template2.templateType && + isContainersEqual(template1.containers, template2.containers); + } + + bool isContainersEqual( + List containers1, + List containers2, + ) { + if (containers1.length != containers2.length) { + return false; + } + for (int i = 0; i < containers1.length; i++) { + if (!isContainerEqual(containers1[i], containers2[i])) { + return false; + } + } + return true; + } + + bool isContainerEqual(Container containers1, Container containers2) { + return containers1.templateType == containers2.templateType && + containers1.id == containers2.id && + isContainerStyleEqual(containers1.style, containers2.style) && + isActionsEqual(containers1.actionList, containers2.actionList) && + isWidgetsEqual(containers1.widgets, containers2.widgets); + } + + bool isContainerStyleEqual(ContainerStyle? style1, ContainerStyle? style2) { + if (style1 == style2) { + return true; + } + if (style1 == null || style2 == null) { + return false; + } + return style1.backgroundColor == style2.backgroundColor; + } + + bool isActionsEqual(List actionList1, List actionList2) { + if (actionList1.length != actionList1.length) { + return false; + } + for (int i = 0; i < actionList1.length; i++) { + if (!isActionEqual(actionList1[i], actionList2[i])) { + return false; + } + } + return true; + } + + bool isActionEqual(Action? action1, Action? action2) { + if (action1 == action2) { + return true; + } + if (action1 == null || action2 == null) { + return false; + } + if (action1 is NavigationAction && action2 is NavigationAction) { + return isNavigationActionEqual(action1, action2); + } + return action1.actionType == action2.actionType; + } + + bool isNavigationActionEqual( + NavigationAction? action1, + NavigationAction? action2, + ) { + if (action1 == action2) { + return true; + } + if (action1 == null || action2 == null) { + return false; + } + return action1.value == action2.value && + mapEquals(action1.keyValuePairs, action2.keyValuePairs) && + action1.navigationType == action2.navigationType && + action1.actionType == action2.actionType; + } + + bool isWidgetsEqual(List widgets1, List widgets2) { + if (widgets1.length != widgets1.length) { + return false; + } + for (int i = 0; i < widgets1.length; i++) { + if (!isWidgetEqual(widgets1[i], widgets2[i])) { + return false; + } + } + return true; + } + + bool isWidgetStyleEqual( + WidgetStyle? widgetStyle1, + WidgetStyle? widgetStyle2, + ) { + if (widgetStyle1 == null && widgetStyle2 == null) { + return true; + } + if (widgetStyle1 == null || widgetStyle2 == null) { + return false; + } + if (widgetStyle1.backgroundColor != widgetStyle2.backgroundColor) { + return false; + } + if (widgetStyle1.runtimeType != widgetStyle2.runtimeType) { + return false; + } + if (widgetStyle1 is ButtonStyle && widgetStyle2 is ButtonStyle) { + return widgetStyle1.fontSize == widgetStyle2.fontSize; + } + if (widgetStyle1 is ImageStyle && widgetStyle2 is ImageStyle) { + return true; + } + if (widgetStyle1 is TextStyle && widgetStyle2 is TextStyle) { + return widgetStyle1.fontSize == widgetStyle2.fontSize; + } + return false; + } + + bool isWidgetEqual(Widget widget1, Widget widget2) { + return isWidgetStyleEqual(widget1.style, widget2.style) && + widget2.id == widget1.id && + widget2.widgetType == widget1.widgetType && + widget1.content == widget2.content && + isActionsEqual(widget1.actionList, widget2.actionList); + } + + bool isMetaDataEqual(MetaData metaData1, MetaData metaData2) { + return metaData1.isNewCard == metaData2.isNewCard && + mapEquals(metaData2.campaignPayload, metaData1.campaignPayload) && + mapEquals(metaData2.metaData, metaData1.metaData) && + metaData1.deletionTime == metaData2.deletionTime && + metaData1.updatedTime == metaData2.updatedTime && + metaData1.createdAt == metaData2.createdAt && + isCampaignStateEqual( + metaData1.campaignState, + metaData2.campaignState, + ) && + isDisplayControlEqual( + metaData1.displayControl, + metaData2.displayControl, + ); + } + + bool isCampaignStateEqual( + CampaignState campaignState1, + CampaignState campaignState2, + ) { + return campaignState1.isClicked == campaignState2.isClicked && + campaignState1.firstReceived == campaignState2.firstReceived && + campaignState1.firstSeen == campaignState2.firstSeen && + campaignState1.totalShowCount == campaignState2.totalShowCount && + campaignState1.localShowCount == campaignState2.localShowCount; + } + + bool isDisplayControlEqual( + DisplayControl displayControl1, + DisplayControl displayControl2, + ) { + return displayControl1.isPinned == displayControl2.isPinned && + displayControl1.expireAfterDelivered == + displayControl2.expireAfterDelivered && + displayControl1.expireAfterSeen == displayControl2.expireAfterSeen && + displayControl1.expireAt == displayControl2.expireAt && + displayControl1.maxCount == displayControl2.maxCount && + isShowTimeEqual(displayControl1.showTime, displayControl2.showTime); + } + + bool isShowTimeEqual(ShowTime showTime1, ShowTime showTime2) { + return showTime1.startTime == showTime2.startTime; + } + + bool isCardInfoEqual(CardsInfo cardsInfo1, CardsInfo cardsInfo2) { + return cardsInfo1.shouldShowAllTab == cardsInfo2.shouldShowAllTab && + const ListEquality() + .equals(cardsInfo1.categories, cardsInfo2.categories) && + isCardsEqual(cardsInfo1.cards, cardsInfo2.cards); + } + + bool isSyncDataEquals( + SyncCompleteData syncCompleteData1, + SyncCompleteData syncCompleteData2, + ) { + return syncCompleteData1.hasUpdates == syncCompleteData2.hasUpdates && + syncCompleteData1.syncType == syncCompleteData2.syncType; + } + + bool isCardsEqual(List cards1, List cards2) { + if (cards1.length != cards2.length) { + return false; + } + for (int i = 0; i < cards1.length; i++) { + if (!isCardEqual(cards1[i], cards2[i])) { + return false; + } + } + return true; + } + + bool isCardDataEquals(CardsData cardsData1, CardsData cardsData2) { + return cardsData1.category == cardsData2.category && + isCardsEqual(cardsData1.cards, cardsData2.cards); + } +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/test/cards_mock_platform.dart b/flutter-sample/cards/moengage_cards_platform_interface/test/cards_mock_platform.dart new file mode 100644 index 00000000..94c412d5 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/test/cards_mock_platform.dart @@ -0,0 +1,6 @@ +import 'package:moengage_cards_platform_interface/src/internal/method_channel_moengage_cards.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Mock Platform Interface. +class MockCardsPlatform extends MethodChannelMoEngageCards + with MockPlatformInterfaceMixin {} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/test/cards_platform_interface_test.dart b/flutter-sample/cards/moengage_cards_platform_interface/test/cards_platform_interface_test.dart new file mode 100644 index 00000000..883bc328 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/test/cards_platform_interface_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; + +import 'cards_mock_platform.dart'; +import 'data_provider/data_model_provider.dart'; +import 'data_provider/data_provider.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final MoEngageCardsPlatformInterface mock = MockCardsPlatform(); + MoEngageCardsPlatformInterface.instance = mock; + const MethodChannel channel = MethodChannel(cardsMethodChannel); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, methodCallHandler); + + test('CardsInfo Test', () async { + final CardsInfo cardsInfo = await mock.getCardsInfo(''); + expect(cardsInfo.categories, ['promotions']); + expect(cardsInfo.shouldShowAllTab, true); + expect(cardsInfo.toJson(), cardsInfoModel.toJson()); + }); + + test('Is All category Enabled Test', () async { + final bool isAllCategoryEnabled = await mock.isAllCategoryEnabled(''); + expect(isAllCategoryEnabled, false); + }); + + test('Get Cards Categories', () async { + final List cardsCategories = await mock.getCardsCategories(''); + expect(cardsCategories, ['promotion', 'announcement']); + }); + + test('New Cards Count', () async { + final int newCardsCount = await mock.getNewCardsCount(''); + expect(newCardsCount, 10); + }); + + test('UnClicked Cards Count', () async { + final int unClickedCardsCount = await mock.getUnClickedCardsCount(''); + expect(unClickedCardsCount, 20); + }); + + test('Get Cards For Category', () async { + final CardsData cardsForCategory = + await mock.getCardsForCategory('promotions', ''); + expect(cardsForCategory.toJson(), cardsDataModel.toJson()); + }); + + test('Fetch All Cards', () async { + final CardsData cardData = await mock.fetchCards(''); + expect(cardData.toJson(), cardDataModel.toJson()); + }); +} + +Future methodCallHandler(MethodCall methodCall) async { + String methodResult = ''; + switch (methodCall.method) { + case methodCardsInfo: + methodResult = cardsInfoJson; + break; + case methodIsAllCategoryEnabled: + methodResult = isAllCategoryEnabledJson; + break; + case methodCardsCategories: + methodResult = cardsCategoriesJson; + break; + case methodNewCardsCount: + methodResult = newCardsCountJson; + break; + case methodUnClickedCardsCount: + methodResult = unClickedCardsCountJson; + break; + case methodCardsForCategory: + methodResult = cardsForCategoryJson; + break; + case methodFetchCards: + methodResult = fetchCardsData; + break; + } + return methodResult; +} diff --git a/flutter-sample/cards/moengage_cards_platform_interface/test/data_provider/data_model_provider.dart b/flutter-sample/cards/moengage_cards_platform_interface/test/data_provider/data_model_provider.dart new file mode 100644 index 00000000..d7b76360 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/test/data_provider/data_model_provider.dart @@ -0,0 +1,103 @@ +import 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; + +final Card cardModel = Card( + id: 210, + cardId: '123457', + category: 'promotions', + template: Template( + templateType: TemplateType.illustration, + kvPairs: {}, + containers: [ + Container( + id: 10, + templateType: TemplateType.illustration, + style: ContainerStyle( + backgroundColor: '#ffffff', + ), + actionList: [ + NavigationAction( + actionType: ActionType.navigate, + navigationType: NavigationType.deepLink, + value: 'https://google.com', + keyValuePairs: {}, + ) + ], + widgets: [ + Widget( + id: 0, + widgetType: WidgetType.image, + content: 'https://picsum.photos/200/200', + style: ImageStyle(backgroundColor: '#1a2a3a'), + actionList: [ + NavigationAction( + actionType: ActionType.navigate, + navigationType: NavigationType.screenName, + value: 'com.moengage.sampleapp.MainActivity', + keyValuePairs: {}, + ) + ], + ), + Widget( + id: 1, + widgetType: WidgetType.text, + content: 'Some Header', + style: TextStyle(backgroundColor: '#ed2a4a', fontSize: 20), + actionList: [], + ), + Widget( + id: 2, + widgetType: WidgetType.text, + content: 'Some message', + style: TextStyle(backgroundColor: '#ed2a4a', fontSize: 20), + actionList: [], + ), + Widget( + id: 3, + widgetType: WidgetType.button, + content: 'CLICK ME!', + style: ButtonStyle(backgroundColor: '#FFFFF', fontSize: 2), + actionList: [], + ), + ], + ) + ], + ), + metaData: MetaData( + campaignState: CampaignState( + localShowCount: 0, + isClicked: false, + firstReceived: 14545545, + firstSeen: 12333566, + totalShowCount: 10, + ), + deletionTime: 12333566, + createdAt: 123456789, + isPinned: false, + displayControl: DisplayControl( + expireAt: 100000, + expireAfterSeen: 1233456, + expireAfterDelivered: 2592000, + maxCount: 10, + isPinned: false, + showTime: ShowTime(startTime: '11:11', endTime: '12:22'), + ), + metaData: {}, + isNewCard: false, + updatedTime: 54768, + campaignPayload: {}, + ), +); + +final CardsInfo cardsInfoModel = CardsInfo( + cards: [cardModel], + categories: ['promotions'], + shouldShowAllTab: true, +); + +final CardsData cardDataModel = CardsData(category: 'All', cards: [cardModel]); + +final CardsData cardsDataModel = + CardsData(category: 'promotions', cards: [cardModel]); + +final SyncCompleteData syncCompleteDataModel = + SyncCompleteData(syncType: SyncType.pullToRefresh, hasUpdates: true); diff --git a/flutter-sample/cards/moengage_cards_platform_interface/test/data_provider/data_provider.dart b/flutter-sample/cards/moengage_cards_platform_interface/test/data_provider/data_provider.dart new file mode 100644 index 00000000..d9bef0b7 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/test/data_provider/data_provider.dart @@ -0,0 +1,728 @@ +const String singleCard = ''' +{ + "id": 210, + "card_id": "123457", + "category": "promotions", + "template_data": { + "type": "illustration", + "kvPairs": { + }, + "containers": [ + { + "id": 10, + "type": "illustration", + "style": { + "bgColor": "#ffffff" + }, + "actions": [ + { + "name": "navigate", + "type": "deepLink", + "value": "https://google.com", + "kvPairs": {} + } + ], + "widgets": [ + { + "id": 0, + "type": "image", + "content": "https://picsum.photos/200/200", + "style": { + "bgColor": "#1a2a3a" + }, + "actions": [ + { + "name": "navigate", + "type": "screenName", + "value": "com.moengage.sampleapp.MainActivity", + "kvPairs": { + } + } + ] + }, + { + "id": 1, + "type": "text", + "content": "Some Header", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 2, + "type": "text", + "content": "Some message", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 3, + "type": "button", + "content": "CLICK ME!", + "style": { + "bgColor": "#FFFFF", + "fontSize": 2 + }, + "actions": [] + } + ] + } + ] + }, + "meta_data": { + "campaignState": { + "localShowCount": 0, + "isClicked": false, + "firstReceived": 14545545, + "firstSeen": 12333566, + "totalShowCount": 10 + }, + "deletionTime": 12333566, + "display_controls": { + "expire_at": 100000, + "expire_after_seen": 1233456, + "expire_after_delivered": 2592000, + "max_times_to_show": 10, + "is_pin": false, + "show_time": { + "start_time": "11:11", + "end_time": "12:22" + } + }, + "metaData": { + }, + "isNewCard": false, + "updated_at": 54768, + "created_at":123456789, + "campaignPayload": { + } + } +} +'''; + +const String cardsInfoJson = ''' +{ + "accountMeta":{ + "appId" : "" + }, + "data":{ + "shouldShowAllTab":true, + "categories": ["promotions"], + "cards":[ + { + "id": 210, + "card_id": "123457", + "category": "promotions", + "template_data": { + "type": "illustration", + "kvPairs": { + }, + "containers": [ + { + "id": 10, + "type": "illustration", + "style": { + "bgColor": "#ffffff" + }, + "actions": [ + { + "name": "navigate", + "type": "deepLink", + "value": "https://google.com", + "kvPairs": {} + } + ], + "widgets": [ + { + "id": 0, + "type": "image", + "content": "https://picsum.photos/200/200", + "style": { + "bgColor": "#1a2a3a" + }, + "actions": [ + { + "name": "navigate", + "type": "screenName", + "value": "com.moengage.sampleapp.MainActivity", + "kvPairs": { + } + } + ] + }, + { + "id": 1, + "type": "text", + "content": "Some Header", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 2, + "type": "text", + "content": "Some message", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 3, + "type": "button", + "content": "CLICK ME!", + "style": { + "bgColor": "#FFFFF", + "fontSize": 2 + }, + "actions": [] + } + ] + } + ] + }, + "meta_data": { + "campaignState": { + "localShowCount": 0, + "isClicked": false, + "firstReceived": 14545545, + "firstSeen": 12333566, + "totalShowCount": 10 + }, + "deletionTime": 12333566, + "display_controls": { + "expire_at": 100000, + "expire_after_seen": 1233456, + "expire_after_delivered": 2592000, + "max_times_to_show": 10, + "is_pin": false, + "show_time": { + "start_time": "11:11", + "end_time": "12:22" + } + }, + "metaData": { + }, + "isNewCard": false, + "updated_at": 54768, + "created_at":123456789, + "campaignPayload": { + } + } +} + ] + } +} +'''; + +const String isAllCategoryEnabledJson = ''' +{ + "accountMeta":{ + "appId" : "" + }, + "data":{ + "isAllCategoryEnabled":false +} +} +'''; + +const String cardsCategoriesJson = ''' +{ + "accountMeta":{ + "appId" : "" + }, + "data":{ + "categories": ["promotion","announcement"] +} +} +'''; + +const String newCardsCountJson = ''' +{ + "accountMeta":{ + "appId" : "" + }, + "data":{ + "newCardsCount": 10 +} +} +'''; + +const String unClickedCardsCountJson = ''' +{ + "accountMeta":{ + "appId" : "" + }, + "data":{ + "unClickedCardsCount": 20 +} +} +'''; + +const String cardsForCategoryJson = ''' +{ + "accountMeta":{ + "appId" : "" + }, + "data":{ + "category":"promotions", + "cards":[ + { + "id": 210, + "card_id": "123457", + "category": "promotions", + "template_data": { + "type": "illustration", + "kvPairs": { + }, + "containers": [ + { + "id": 10, + "type": "illustration", + "style": { + "bgColor": "#ffffff" + }, + "actions": [ + { + "name": "navigate", + "type": "deepLink", + "value": "https://google.com", + "kvPairs": {} + } + ], + "widgets": [ + { + "id": 0, + "type": "image", + "content": "https://picsum.photos/200/200", + "style": { + "bgColor": "#1a2a3a" + }, + "actions": [ + { + "name": "navigate", + "type": "screenName", + "value": "com.moengage.sampleapp.MainActivity", + "kvPairs": { + } + } + ] + }, + { + "id": 1, + "type": "text", + "content": "Some Header", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 2, + "type": "text", + "content": "Some message", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 3, + "type": "button", + "content": "CLICK ME!", + "style": { + "bgColor": "#FFFFF", + "fontSize": 2 + }, + "actions": [] + } + ] + } + ] + }, + "meta_data": { + "campaignState": { + "localShowCount": 0, + "isClicked": false, + "firstReceived": 14545545, + "firstSeen": 12333566, + "totalShowCount": 10 + }, + "deletionTime": 12333566, + "display_controls": { + "expire_at": 100000, + "expire_after_seen": 1233456, + "expire_after_delivered": 2592000, + "max_times_to_show": 10, + "is_pin": false, + "show_time": { + "start_time": "11:11", + "end_time": "12:22" + } + }, + "metaData": { + }, + "isNewCard": false, + "updated_at": 54768, + "created_at":123456789, + "campaignPayload": { + } + } +} + ] + } +} +'''; + +const String cardsData = ''' +{ + "category":"promotions", + "cards":[ + { + "id": 210, + "card_id": "123457", + "category": "promotions", + "template_data": { + "type": "illustration", + "kvPairs": { + }, + "containers": [ + { + "id": 10, + "type": "illustration", + "style": { + "bgColor": "#ffffff" + }, + "actions": [ + { + "name": "navigate", + "type": "deepLink", + "value": "https://google.com", + "kvPairs": {} + } + ], + "widgets": [ + { + "id": 0, + "type": "image", + "content": "https://picsum.photos/200/200", + "style": { + "bgColor": "#1a2a3a" + }, + "actions": [ + { + "name": "navigate", + "type": "screenName", + "value": "com.moengage.sampleapp.MainActivity", + "kvPairs": { + } + } + ] + }, + { + "id": 1, + "type": "text", + "content": "Some Header", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 2, + "type": "text", + "content": "Some message", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 3, + "type": "button", + "content": "CLICK ME!", + "style": { + "bgColor": "#FFFFF", + "fontSize": 2 + }, + "actions": [] + } + ] + } + ] + }, + "meta_data": { + "campaignState": { + "localShowCount": 0, + "isClicked": false, + "firstReceived": 14545545, + "firstSeen": 12333566, + "totalShowCount": 10 + }, + "deletionTime": 12333566, + "display_controls": { + "expire_at": 100000, + "expire_after_seen": 1233456, + "expire_after_delivered": 2592000, + "max_times_to_show": 10, + "is_pin": false, + "show_time": { + "start_time": "11:11", + "end_time": "12:22" + } + }, + "metaData": { + }, + "isNewCard": false, + "updated_at": 54768, + "created_at":123456789, + "campaignPayload": { + } + } +}] +} +'''; + +const String cardsInfoData = ''' +{ + "shouldShowAllTab":true, + "categories": ["promotions"], + "cards":[ + { + "id": 210, + "card_id": "123457", + "category": "promotions", + "template_data": { + "type": "illustration", + "kvPairs": { + }, + "containers": [ + { + "id": 10, + "type": "illustration", + "style": { + "bgColor": "#ffffff" + }, + "actions": [ + { + "name": "navigate", + "type": "deepLink", + "value": "https://google.com", + "kvPairs": {} + } + ], + "widgets": [ + { + "id": 0, + "type": "image", + "content": "https://picsum.photos/200/200", + "style": { + "bgColor": "#1a2a3a" + }, + "actions": [ + { + "name": "navigate", + "type": "screenName", + "value": "com.moengage.sampleapp.MainActivity", + "kvPairs": { + } + } + ] + }, + { + "id": 1, + "type": "text", + "content": "Some Header", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 2, + "type": "text", + "content": "Some message", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 3, + "type": "button", + "content": "CLICK ME!", + "style": { + "bgColor": "#FFFFF", + "fontSize": 2 + }, + "actions": [] + } + ] + } + ] + }, + "meta_data": { + "campaignState": { + "localShowCount": 0, + "isClicked": false, + "firstReceived": 14545545, + "firstSeen": 12333566, + "totalShowCount": 10 + }, + "deletionTime": 12333566, + "display_controls": { + "expire_at": 100000, + "expire_after_seen": 1233456, + "expire_after_delivered": 2592000, + "max_times_to_show": 10, + "is_pin": false, + "show_time": { + "start_time": "11:11", + "end_time": "12:22" + } + }, + "metaData": { + }, + "isNewCard": false, + "updated_at": 54768, + "created_at":123456789, + "campaignPayload": { + } + } +} + ] + } +'''; + +const String syncCompleteData = ''' +{ +"hasUpdates": true, +"syncType": "PULL_TO_REFRESH" +} +'''; + +const String fetchCardsData = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "cards": [ + { + "id": 210, + "card_id": "123457", + "category": "promotions", + "template_data": { + "type": "illustration", + "kvPairs": {}, + "containers": [ + { + "id": 10, + "type": "illustration", + "style": { + "bgColor": "#ffffff" + }, + "actions": [ + { + "name": "navigate", + "type": "deepLink", + "value": "https://google.com", + "kvPairs": {} + } + ], + "widgets": [ + { + "id": 0, + "type": "image", + "content": "https://picsum.photos/200/200", + "style": { + "bgColor": "#1a2a3a" + }, + "actions": [ + { + "name": "navigate", + "type": "screenName", + "value": "com.moengage.sampleapp.MainActivity", + "kvPairs": {} + } + ] + }, + { + "id": 1, + "type": "text", + "content": "Some Header", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 2, + "type": "text", + "content": "Some message", + "style": { + "bgColor": "#ed2a4a", + "fontSize": 20 + }, + "actions": [] + }, + { + "id": 3, + "type": "button", + "content": "CLICK ME!", + "style": { + "bgColor": "#FFFFF", + "fontSize": 2 + }, + "actions": [] + } + ] + } + ] + }, + "meta_data": { + "campaignState": { + "localShowCount": 0, + "isClicked": false, + "firstReceived": 14545545, + "firstSeen": 12333566, + "totalShowCount": 10 + }, + "deletionTime": 12333566, + "display_controls": { + "expire_at": 100000, + "expire_after_seen": 1233456, + "expire_after_delivered": 2592000, + "max_times_to_show": 10, + "is_pin": false, + "show_time": { + "start_time": "11:11", + "end_time": "12:22" + } + }, + "metaData": {}, + "isNewCard": false, + "updated_at": 54768, + "created_at": 123456789, + "campaignPayload": {} + } + } + ] + } +} +'''; diff --git a/flutter-sample/cards/moengage_cards_platform_interface/test/parser_test.dart b/flutter-sample/cards/moengage_cards_platform_interface/test/parser_test.dart new file mode 100644 index 00000000..08d06d99 --- /dev/null +++ b/flutter-sample/cards/moengage_cards_platform_interface/test/parser_test.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:moengage_cards_platform_interface/moengage_cards_platform_interface.dart'; +import 'cards_comparator.dart'; +import 'data_provider/data_model_provider.dart'; +import 'data_provider/data_provider.dart'; + +void main() { + test('Test Card Model toJson Payload Mapper', () { + expect(jsonDecode(singleCard), cardModel.toJson()); + }); + + test('Test Cards Data Model fromJson Parsing', () { + final Card actual = + Card.fromJson(jsonDecode(singleCard) as Map); + expect(CardsComparator().isCardEqual(actual, cardModel), true); + }); + + test('Test Cards Info Model toJson Payload Mapper', () { + expect(jsonDecode(cardsInfoData), cardsInfoModel.toJson()); + }); + + test('Test Card Model fromJson Parsing', () { + final CardsInfo actual = + CardsInfo.fromJson(jsonDecode(cardsInfoData) as Map); + expect(CardsComparator().isCardInfoEqual(actual, cardsInfoModel), true); + }); + + test('Test Cards Data Model toJson Payload Mapper', () { + expect(jsonDecode(cardsData), cardsDataModel.toJson()); + }); + + test('Test Card Model From Json', () { + final CardsData actual = + CardsData.fromJson(jsonDecode(cardsData) as Map); + expect(CardsComparator().isCardDataEquals(actual, cardsDataModel), true); + }); + + test('Test Sync Complete Data toJson Payload Mapper', () { + expect(jsonDecode(syncCompleteData), syncCompleteDataModel.toJson()); + }); + + test('Test Cards Info Model fromJson Parsing', () { + final SyncCompleteData actual = SyncCompleteData.fromJson( + jsonDecode(syncCompleteData) as Map, + ); + expect( + CardsComparator().isSyncDataEquals(actual, syncCompleteDataModel), + true, + ); + }); +} diff --git a/flutter-sample/core/.gitignore b/flutter-sample/core/.gitignore new file mode 100644 index 00000000..9681fe2c --- /dev/null +++ b/flutter-sample/core/.gitignore @@ -0,0 +1,77 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/.settings + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +pubspec.lock diff --git a/flutter-sample/core/moengage_flutter/CHANGELOG.md b/flutter-sample/core/moengage_flutter/CHANGELOG.md new file mode 100644 index 00000000..4a15873e --- /dev/null +++ b/flutter-sample/core/moengage_flutter/CHANGELOG.md @@ -0,0 +1,269 @@ +# MoEngage Flutter Plugin + +# 07-12-2023 + +## 6.1.0 +- Added support for array in user attributes +- Android + - Google Policy - Delete User details API + - Native SDK Updated to support version `12.10.01` and above + +# 13-09-2023 + +## 6.0.0 +- Federated Plugin Implementation +- Breaking Change: Use import 'package:moengage_flutter/moengage_flutter.dart'; to import any files in the `moengage_flutter` package. Remove the existing import related to `moengage_flutter` +- Android + - Native SDK Updated to support version `12.9.00` and above + +# 26-07-2023 + +## 5.5.1 +- Android + - BugFix: + - Self Handled InApp delivery controls not working. + +# 19-07-2023 + +## 5.5.0 +- Android + - Plugin Base Version Updated to `3.3.2` +- iOS + - MoEngage-iOS-SDK version updated to `~>9.10.0` + +# 31-05-2023 + +## 5.4.0 +- Android + - Compile SDK Version Updated to 33 + - Native SDK updated to support version `12.9.00` and above. + - Support for Foreground Push Click Callback +- iOS + - MoEngage-iOS-SDK version updated to `~>9.8.0` + +# 16-05-2023 + +## 5.3.1 +- Android + - SelfHandled InApp Callback for Test InApps and Event Triggered InApps + +# 08-02-2023 +## 5.3.0 +- Security improvement: controlled logging for release, debug and profile mode +- Android + - Support for 2 Step Push Optin InApps + - Device Id enable / disable support +- iOS + - MoEngage-iOS-SDK version updated to `~>9.4.0`. + +# 23-01-2023 +## 5.2.0 +- iOS + - MoEngage-iOS-SDK version updated to `~>9.2.0`. + - Updated API + + | Then | Now | + |:----------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:| + | public func initializeDefaultInstance(config: MOSDKConfig, sdkState: MoEngageSDKState = .enabled, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) |public func initializeDefaultInstance(config: MoEngageSDKConfig, sdkState: MoEngageSDKState = .enabled, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) | + | public func initializeDefaultInstance(_ config: MOSDKConfig, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) | public func initializeDefaultInstance(_ config: MoEngageSDKConfig, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) | + +# 04-11-2022 + +## 5.1.1 +- Bugfix + - Typo fix in API name `enableAdIdTracking()` + +# 27-10-2022 + +## 5.1.0 +- Android + - Support for Android 13 notification permission. + - Android Gradle Plugin version updated to `7.3.1` + - Gradle version updated to `7.4` + - Build Configuration Updates + - Compile SDK Version - 31 + - Target SDK version - 31 + - Support for Android SDK version `12.4.00` + - InApp `6.4.0` +- iOS + - Deprecated API + +| Then | Now | +|:------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------:| +| initializeDefaultInstance(_ config: MOSDKConfig, sdkState: Bool = true, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) | initializeDefaultInstance(config: MOSDKConfig, sdkState: MoEngageSDKState = .enabled, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) | + + +## 27.09.2022 + +### 5.0.0 +- Support for Android SDK version `12.2.05` and above. +- Support for iOS SDK version `8.3.1` and above. +- Breaking Changes + +| Then | Now | +|---------------------------|------------------------------------| +| MoEngageFlutter() | MoEngageFlutter("YOUR_APP_ID") | +| optOutDataTracking(false) | enableDataTracking() | +| optOutDataTracking(true) | disableDataTracking() | + + - InApp Model `InAppCampaign` broken down from a single object to multiple objects + - `InAppData` + - `ClickData` + - `SelfHandledCampaignData` + - InApp `setUpInAppCallbacks` removed and callbacks broken down into multiple listeners. + +| Callback Type | Method | +|------------------------------------------|-----------------------------------------------------------------| +| InApp Shown | setInAppShownCallbackHandler(InAppShownCallbackHandler) | +| InApp Dismissed | setInAppDismissedCallbackHandler(InAppDismissedCallbackHandler) | +| InApp Clicked(Navigation, Custom Action) | setInAppClickHandler(InAppClickCallbackHandler) | +| InApp Self Handled | setSelfHandledInAppHandler(SelfHandledInAppCallbackHandler) | + + - Push campaign Model is restructured and renamed from `PushCampaign` to `PushCampaignData` + - Push callback APIs are renamed. + +| Then | Now | +|--------------------------------------------------|-------------------------------------------------------| +| setUpPushCallbacks(PushCallbackHandler) | setPushClickCallbackHandler(PushClickCallbackHandler) | +| setUpPushTokenCallback(PushTokenCallbackHandler) | setPushTokenCallbackHandler(PushTokenCallbackHandler) | + +- Removed APIs + +| Removed APIs | +|-----------------------------| +| selfHandledPrimaryClicked() | +| enableSDKLogs() | +| optOutInAppNotification() | +| optOutPushNotification() | +| startGeofenceMonitoring() | + +- Android + - Build Configuration Updates + - Minimum SDK version - 21 + - Target SDK version - 30 + - Compile SDK Version - 30 + - Mi SDK update to Version 5.x.x, refer to the [Configuring Xiaomi Push](https://developers.moengage.com/hc/en-us/articles/4403466194708) and update the integration. + - Removed and replaced APIs + +| Then | Now | +|---------------------------------------------------------------|---------------------------------------------------------------------| +| MoEInitializer.initialize(Context, MoEngage.Builder) | MoEInitializer.initialiseDefaultInstance(Context, MoEngage.Builder) | +| MoEInitializer.initialize(Context, MoEngage.Builder, Boolean) | MoEInitializer.initialiseDefaultInstance(Context, MoEngage.Builder) | + +- iOS + - `MOFlutterInitializer` has been renamed to `MoEngageInitializer` + +| Then | Now | +|-------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| initializeWithSDKConfig(_ config: MOSDKConfig, andLaunchOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) | initializeDefaultInstance(_ config: MOSDKConfig, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) | +| initializeWithSDKConfig(_ config: MOSDKConfig, withSDKState state:Bool, andLaunchOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) | initializeDefaultInstance(_ config: MOSDKConfig, sdkState: Bool = true, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) | + + +### 4.2.0 (29th July 2022) +- Added Flutter 3 support +- Device identifier tracking update as per Google's User Data policy. Advertising Id is only tracked after user consent. +- Android + - Native SDK updated to support version `11.6.02` and above. + +### 4.1.0 *(6th September 2021)* +- Support for HTML InApps +- Android + - Native SDK updated to support version `11.4.00` and above. +- iOS + - Native Dependencies updated to support MoEngage-iOS-SDK `7.1.0` and above + +### 4.0.2 (21st July 2021) +- Bugfix + - Calling MoEngage APIs when application in background is not working on Android. + +### 4.0.1 *(15th June 2021)* +- Bugfix: + - Push click notification callback is not received when clicked action is null. + +### 4.0.0 *(12th May 2021)* +- Migrated the main library to null safety. + - Require Dart 2.12 or greater. +- Bumped flutter dependency constraint to min version `1.17.0` +- Android + - Native SDK updated to support version `11.2.00` and above. + +### 3.0.1 *(28th April 2021)* +- iOS + - Added ObjC support for `MOFlutterInitializer` class + - PodSpec changes to set deployment target to iOS 10.0. + +### 3.0.0 *(26th February 2021)* +- Android + - Native SDK updated to support version `11.0.04` and above. + - Plugin Base `2.0.00` +- iOS + - Plugin now supports iOS 10.0 and above. + - Native Dependencies updated to support MoEngage-iOS-SDK `7.*` and above + - Base plugin version dependency updated to `~> 2.0.2`. +- Added Dart APIs to enable and disable MoEngage Sdk. +- Added Dart API to register a callback for push token generated event. + + +### 2.0.3 *(15th February 2021)* +- Android artifacts use maven central instead of Jcenter. +- Native SDK version `10.6.01` +- Plugin Base `1.2.01` + +### 2.0.2 *(7th December 2020)* + +- Android Base plugin update for enabling callback extension. +- Android Native SDK updated to `10.5.00` + +### 2.0.1 *(6th November, 2020)* + +- Bugfix: AppStatus method was getting called for other channel method calls as well. + + +### 2.0.0 *(23rd October, 2020)* + +- Support for Self-Handled In-App +- Support for In-App V3 +- Android SDK updated to support `10.4.03` and above. +- iOS SDK dependency changed to support versions greater than `6.0.0`. +- Deprecated APIs + +| Then | Now | +|:---------------------------------------------------: |:----------------------------------------------: | +| MoEProperties().addInteger(String, int) | MoEProperties().addAttribute.(String, dynamic) | +| MoEProperties().addString(String, String) | MoEProperties().addAttribute(String, dynamic) | +| MoEProperties().addBoolean(String, bool) | MoEProperties().addAttribute(String, dynamic) | +| MoEProperties().addDouble(String, double) | MoEProperties().addAttribute(String, dynamic) | +| MoEProperties().addLocation(String, MoEGeoLocation) | MoEProperties().addAttribute(String, dynamic) | + +- Removed APIs + +| Then | Now | +|:---------------------------------------------------: |:----------------------------------------------: | +| onPushClick(Map) | onPushClick(PushCampaign) | +| onInAppClick(Map) | onInAppClick(InAppCampaign) | +| onInAppShown(Map) | onInAppShown(InAppCampaign) | +| onInAppShown(Map message) | onInAppShown(InAppCampaign message) | +| passPushToken(String) | passFCMPushToken(String) | +| passPushPayload(Map) | passFCMPushPayload(Map) | + +- Removed APIs Android + +| Then | Now | +|:-----------------------------------:|:----------------------------------------------------:| +| MoEInitializer.initialize(MoEngage) | MoEInitializer.initialize(Context, MoEngage.Builder) | + + +### 1.1.0 *(10th February, 2020)* + +- Add Dart APIs for passing FCM Push Token and FCM Push Payload from Android Platform. + + +### 1.0.1 *(17th December, 2019)* +- Sample Updated +- ReadMe Updated +- Improved logging + + +### 1.0.0 *(16th December, 2019)* + +- Initial Release diff --git a/flutter-sample/core/moengage_flutter/LICENSE b/flutter-sample/core/moengage_flutter/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/core/moengage_flutter/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter/README.md b/flutter-sample/core/moengage_flutter/README.md new file mode 100644 index 00000000..273a6fb8 --- /dev/null +++ b/flutter-sample/core/moengage_flutter/README.md @@ -0,0 +1,76 @@ +# MoEngage Flutter Plugin + +Flutter Plugin for MoEngage Platform + +## SDK Installation + +To add the MoEngage Flutter SDK to your application, edit your application's `pubspec.yaml` file and add the below dependency to it: + +![Download](https://img.shields.io/pub/v/moengage_flutter.svg) + +```yaml +dependencies: + moengage_flutter: $latestSdkVersion +``` +replace `$latestSdkVersion` with the latest SDK version. + + Run flutter packages get to install the SDK. + + ### Android Installation + +![MavenBadge](https://maven-badges.herokuapp.com/maven-central/com.moengage/moe-android-sdk/badge.svg) + + Once you install the Flutter Plugin add MoEngage's native Android SDK dependency to the Android project of your application. + Navigate to `android --> app --> build.gradle`. Add the MoEngage Android SDK's dependency in the `dependencies` block + + ```groovy + dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation("androidx.core:core:1.9.0") + implementation("androidx.appcompat:appcompat:1.4.0") + implementation("androidx.lifecycle:lifecycle-process:2.5.1") + implementation "com.moengage:moe-android-sdk:$sdkVersion" +} + ``` +where `$sdkVersion` should be replaced by the latest version of the MoEngage SDK. + +## SDK Initialization + +### Android SDK Initialization +Get APP ID from the Settings Page on the MoEngage dashboard and initialize the MoEngage SDK in the `Application` class's `onCreate()` + +```kotlin +// this is the instance of the application class and "XXXXXXXXXXX" is the APP ID from the dashboard. +val moEngage = MoEngage.Builder(this, "XXXXXXXXXXX") +MoEInitializer.initialize(appicationContext, builder) +``` +Refer to the [API reference doc](https://moengage.github.io/android-api-reference/) for a detailed list of possible configurations. + +``` +Note: +All the configuration should be added to the builder before calling initialize. If you are calling initialize at multiple places please ensure that all the required flags and configuration are set each time you initialize to maintain consistency in behavior. +``` + +### iOS SDK Initialization + +Make sure to run `flutter build` command to make sure all the CocoaPods dependencies are added to your project. (i.e, `MoEngage-iOS-SDK` and `moengage_flutter`) + +To initialize the iOS Application with the MoEngage App ID from Settings in Dashboard. In your project, go to AppDelegate file and call the initialize method of `MOFlutterInitializer` instance in applicationdidFinishLaunchingWithOptions() method as shown below: + +```swift + var sdkConfig : MOSDKConfig + let yourAppID = "Your App ID" //App ID: You can be obtain it from App Settings in MoEngage Dashboard. + if let config = MoEngage.sharedInstance().getDefaultSDKConfiguration() { + sdkConfig = config + sdkConfig.moeAppID = yourAppID + } + else{ + sdkConfig = MOSDKConfig.init(appID: yourAppID) + } + sdkConfig.appGroupID = "Your App Group ID" + sdkConfig.moeDataCenter = // use MODataCenter enum to set the datacenter for your account + + MOFlutterInitializer.sharedInstance.initializeWithSDKConfig(sdkConfig, andLaunchOptions: launchOptions) +``` + +Refer to the [Documentation](https://developers.moengage.com/hc/en-us/categories/4404300700308-Flutter-SDK) for complete integration guide. diff --git a/flutter-sample/core/moengage_flutter/config.json b/flutter-sample/core/moengage_flutter/config.json new file mode 100644 index 00000000..dd87792f --- /dev/null +++ b/flutter-sample/core/moengage_flutter/config.json @@ -0,0 +1,3 @@ +{ + "version": "6.1.0" +} \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter/example/README.md b/flutter-sample/core/moengage_flutter/example/README.md new file mode 100644 index 00000000..08725b30 --- /dev/null +++ b/flutter-sample/core/moengage_flutter/example/README.md @@ -0,0 +1,3 @@ +# moengage_flutter_example + +Demonstrates how to use the moengage_flutter plugin. diff --git a/flutter-sample/core/moengage_flutter/lib/moengage_flutter.dart b/flutter-sample/core/moengage_flutter/lib/moengage_flutter.dart new file mode 100644 index 00000000..56df3105 --- /dev/null +++ b/flutter-sample/core/moengage_flutter/lib/moengage_flutter.dart @@ -0,0 +1,2 @@ +export 'package:moengage_flutter/src/moengage_flutter.dart'; +export 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; diff --git a/flutter-sample/core/moengage_flutter/lib/src/moengage_flutter.dart b/flutter-sample/core/moengage_flutter/lib/src/moengage_flutter.dart new file mode 100644 index 00000000..a551f062 --- /dev/null +++ b/flutter-sample/core/moengage_flutter/lib/src/moengage_flutter.dart @@ -0,0 +1,399 @@ +// ignore_for_file: use_setters_to_change_properties +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; + +/// Helper Class to interact with MoEngage SDK +class MoEngageFlutter { + /// [MoEngageFlutter] Constructor + MoEngageFlutter(this.appId, {MoEInitConfig? moEInitConfig}) + : _moEInitConfig = moEInitConfig ?? MoEInitConfig.defaultConfig() { + //Requires For Setting Up Native to Hybrid Method Channel Callback + CoreController.init(); + } + + /// MoEngage App ID + String appId; + final MoEInitConfig _moEInitConfig; + + MoEngageFlutterPlatform get _platform => MoEngageFlutterPlatform.instance; + + /// Initialize MoEngage SDK + void initialise() { + _platform.initialise(_moEInitConfig, appId); + } + + /// Sets Push Click Callback Handler + /// [handler] - Callback of type [PushClickCallbackHandler] + void setPushClickCallbackHandler(PushClickCallbackHandler? handler) { + CoreInstanceProvider() + .getCallbackCacheForInstance(appId) + .pushClickCallbackHandler = handler; + } + + /// Sets Push Token Available Callback Handler + /// [handler] - Callback of type [PushTokenCallbackHandler] + void setPushTokenCallbackHandler(PushTokenCallbackHandler? handler) { + Cache().pushTokenCallbackHandler = handler; + } + + /// Sets InApp Click Callback Listener + /// [handler] - Callback of type [InAppClickCallbackHandler] + void setInAppClickHandler(InAppClickCallbackHandler? handler) { + CoreInstanceProvider() + .getCallbackCacheForInstance(appId) + .inAppClickCallbackHandler = handler; + } + + /// Sets InApp Shown Callback Handler + /// [handler] - Callback of type [InAppShownCallbackHandler] + void setInAppShownCallbackHandler(InAppShownCallbackHandler? handler) { + CoreInstanceProvider() + .getCallbackCacheForInstance(appId) + .inAppShownCallbackHandler = handler; + } + + /// Sets InApp Dismiss Callback Handler + /// [handler] - Callback of type [InAppDismissedCallbackHandler] + void setInAppDismissedCallbackHandler( + InAppDismissedCallbackHandler? handler) { + CoreInstanceProvider() + .getCallbackCacheForInstance(appId) + .inAppDismissedCallbackHandler = handler; + } + + /// Sets Self Handled Callback Available Handler + /// [handler] - Callback of type [SelfHandledInAppCallbackHandler] + void setSelfHandledInAppHandler(SelfHandledInAppCallbackHandler? handler) { + CoreInstanceProvider() + .getCallbackCacheForInstance(appId) + .selfHandledInAppCallbackHandler = handler; + } + + /// Tracks an event with the given attributes. + /// [eventName] - Name of the Event to be tracked + /// [eventAttributes] - Instance of [MoEProperties] + void trackEvent(String eventName, [MoEProperties? eventAttributes]) { + eventAttributes ??= MoEProperties(); + _platform.trackEvent(eventName, eventAttributes, appId); + } + + /// Set a unique identifier for a user.
+ /// [uniqueId] - Unique Identifier of type [String] + void setUniqueId(String uniqueId) { + _platform.setUniqueId(uniqueId, appId); + } + + /// Update user's unique id which was previously set by setUniqueId(). + /// [newUniqueId] - Unique Identifier of type [String] + void setAlias(String newUniqueId) { + _platform.setAlias(newUniqueId, appId); + } + + /// Tracks user-name as a user attribute. + /// [userName] Full Name value passed by user + void setUserName(String userName) { + _platform.setUserName(userName, appId); + } + + /// Tracks first name as a user attribute. + /// [firstName] First Name of user passed by user + void setFirstName(String firstName) { + _platform.setFirstName(firstName, appId); + } + + /// Tracks last name as a user attribute. + /// [lastName] - Last Name of the User + void setLastName(String lastName) { + _platform.setLastName(lastName, appId); + } + + /// Tracks user's email-id as a user attribute. + /// [emailId] - Email Id of the User + void setEmail(String emailId) { + _platform.setEmail(emailId, appId); + } + + /// Tracks phone number as a user attribute. + /// [phoneNumber] - Phone Number of the User + void setPhoneNumber(String phoneNumber) { + _platform.setPhoneNumber(phoneNumber, appId); + } + + /// Tracks gender as a user attribute. + /// [gender] - Instance of [MoEGender] + void setGender(MoEGender gender) { + _platform.setGender(gender, appId); + } + + /// Set's user's location + /// [location] - Instance of [MoEGeoLocation] + void setLocation(MoEGeoLocation location) { + _platform.setLocation(location, appId); + } + + /// Set user's birth-date. + /// Birthdate should be sent in the following format - yyyy-MM-dd'T'HH:mm:ss.fff'Z' + /// [birthDate] - ISO Formatted Date String + void setBirthDate(String birthDate) { + _platform.setBirthDate(birthDate, appId); + } + + /// Tracks a user attribute. + /// Supported attribute types: + /// - `String` `int`, `double`, `num`, `bool` + /// - `List`, `List`, `List`, `List` + /// [userAttributeValue] - Data of type [dynamic] + /// [userAttributeName] - Name of User Attribute + void setUserAttribute(String userAttributeName, dynamic userAttributeValue) { + if (userAttributeName.isEmpty) { + Logger.w('User Attribute Name cannot be empty'); + return; + } + if (userAttributeValue is String || + userAttributeValue is int || + userAttributeValue is double || + userAttributeValue is num || + userAttributeValue is bool || + userAttributeValue is List || + userAttributeValue is List || + userAttributeValue is List || + userAttributeValue is List) { + _platform.setUserAttribute(userAttributeName, userAttributeValue, appId); + } else { + Logger.w( + 'Only String, Numbers, Bool and List of Strings/Numbers(non-optional) values supported as User Attributes, provided name: $userAttributeName, value: $userAttributeValue'); + } + } + + /// Tracks the given time as user-attribute.
+ /// Date should be passed in the following format - yyyy-MM-dd'T'HH:mm:ss.fff'Z' + /// [userAttributeName] - Name of User Attribute + /// [isoDateString] - ISO Formatted Date String + void setUserAttributeIsoDate(String userAttributeName, String isoDateString) { + _platform.setUserAttributeIsoDate(userAttributeName, isoDateString, appId); + } + + /// Tracks the given location as user attribute. + /// [userAttributeName] - Name of User Attribute + /// [location] - Instance of [MoEGeoLocation] + void setUserAttributeLocation( + String userAttributeName, MoEGeoLocation location) { + _platform.setUserAttributeLocation(userAttributeName, location, appId); + } + + /// This API tells the SDK whether it is a fresh install or an existing application was updated. + /// [appStatus] - Instance of [MoEAppStatus] + void setAppStatus(MoEAppStatus appStatus) { + _platform.setAppStatus(appStatus, appId); + } + + /// Try to show an InApp Message. + void showInApp() { + _platform.showInApp(appId); + } + + /// Invalidates the existing user and session. A new user + /// and session is created. + void logout() { + _platform.logout(appId); + } + + /// Try to return a self handled in-app to the callback listener. + /// Ensure self handled in-app listener is set using [setSelfHandledInAppHandler] + /// before you call this API + void getSelfHandledInApp() { + _platform.getSelfHandledInApp(appId); + } + + /// Mark self-handled campaign as shown. + /// API to be called only when in-app is self handled + /// [data] - Instance of [SelfHandledCampaignData] + void selfHandledShown(SelfHandledCampaignData data) { + final Map payload = InAppPayloadMapper() + .selfHandleCampaignDataToMap(data, selfHandledActionShown); + _platform.selfHandledCallback(payload); + } + + /// Mark self-handled campaign as clicked. + /// API to be called only when in-app is self handled + /// [data] - Instance of [SelfHandledCampaignData] + void selfHandledClicked(SelfHandledCampaignData data) { + final Map payload = InAppPayloadMapper() + .selfHandleCampaignDataToMap(data, selfHandledActionClick); + _platform.selfHandledCallback(payload); + } + + /// Mark self-handled campaign as dismissed. + /// API to be called only when in-app is self handled + /// [data] - Instance of [SelfHandledCampaignData] + void selfHandledDismissed(SelfHandledCampaignData data) { + final Map payload = InAppPayloadMapper() + .selfHandleCampaignDataToMap(data, selfHandledActionDismissed); + _platform.selfHandledCallback(payload); + } + + ///Set the current context for the given user for InApps + /// [contexts] - [List] of Context + void setCurrentContext(List contexts) { + _platform.setCurrentContext(contexts, appId); + } + + /// Reset Current Context for InApps + void resetCurrentContext() { + _platform.resetCurrentContext(appId); + } + + ///Optionally opt-in data tracking. + ///Note: By default data tracking is enabled, this API should be called only + ///if you have called [disableDataTracking] at some point. + void enableDataTracking() { + _platform.optOutDataTracking(false, appId); + } + + ///Optionally opt-out of data tracking. When data tracking is opted-out no + ///event or user attribute is tracked on MoEngage Platform. + void disableDataTracking() { + _platform.optOutDataTracking(true, appId); + } + + /// Push Notification Registration + /// Note: This API is only for iOS Platform. + void registerForPushNotification() { + _platform.registerForPushNotification(); + } + + /// Pass FCM Push Token to the MoEngage SDK. + /// Note: This API is only for Android Platform. + /// [pushToken] - FCM Push Token + void passFCMPushToken(String pushToken) { + _platform.passPushToken(pushToken, MoEPushService.fcm, appId); + } + + /// Pass FCM Push Payload to the MoEngage SDK. + /// Note: This API is only for Android Platform. + /// [payload] - FCM Push Payload Data + void passFCMPushPayload(Map payload) { + _platform.passPushPayload(payload, MoEPushService.fcm, appId); + } + + /// Pass Push Kit Token to the MoEngage SDK. + /// Note: This API is only for Android Platform. + /// [pushToken] - Push Kit Token + void passPushKitPushToken(String pushToken) { + _platform.passPushToken(pushToken, MoEPushService.push_kit, appId); + } + + /// API to enable SDK usage. + /// Note: By default the SDK is enabled, should only be called + /// if you have called [disableSdk] at some point. + void enableSdk() { + _platform.updateSdkState(true, appId); + } + + /// API to disable all features of the SDK. + void disableSdk() { + _platform.updateSdkState(false, appId); + } + + /// To be called when Orientation of the App Is Changed + /// Note: This API is only for Android Platform. + void onOrientationChanged() { + _platform.onOrientationChanged(); + } + + ///API to enable Android-id tracking + /// Note: This API is only for Android Platform. + void enableAndroidIdTracking() { + _platform.updateDeviceIdentifierTrackingStatus(appId, keyAndroidId, true); + } + + ///API to enable Android-id tracking. + ///By default Android-id tracking is disabled, call this method only if you + ///have called [enableAndroidIdTracking] at some point. + /// Note: This API is only for Android Platform. + void disableAndroidIdTracking() { + _platform.updateDeviceIdentifierTrackingStatus(appId, keyAndroidId, false); + } + + ///API to enable Advertising Id tracking + /// Note: This API is only for Android Platform. + void enableAdIdTracking() { + _platform.updateDeviceIdentifierTrackingStatus(appId, keyAdId, true); + } + + ///API to disable Advertising Id tracking. + ///By default Advertising Id tracking is disabled, call this method only if + ///you have enabled Advertising Id tracking at some point + /// Note: This API is only for Android Platform. + void disableAdIdTracking() { + _platform.updateDeviceIdentifierTrackingStatus(appId, keyAdId, false); + } + + ///API to create notification channels on Android. + /// Note: This API is only for Android Platform. + void setupNotificationChannelsAndroid() { + _platform.setupNotificationChannel(); + } + + /// Notify the SDK on notification permission granted state to the application + /// true if granted, else false + /// Note: This API is only for Android Platform. + /// [isGranted] - Push Permission Granted Flag + void pushPermissionResponseAndroid(bool isGranted) { + _platform.permissionResponse(isGranted, PermissionType.PUSH); + } + + /// Navigates the user to the Notification settings on Android 8 or above, + /// on older versions the user is navigated the application settings or + /// application info screen. + /// Note: This API is only for Android Platform. + void navigateToSettingsAndroid() { + _platform.navigateToSettings(); + } + + /// Requests the push permission on Android 13 and above. + /// Note: This API is only for Android Platform. + void requestPushPermissionAndroid() { + _platform.requestPushPermission(); + } + + /// Setup a callback handler for getting the response permission + /// [handler] - Instance of [PermissionResultCallbackHandler] + void setPermissionCallbackHandler(PermissionResultCallbackHandler? handler) { + Cache().permissionResultCallbackHandler = handler; + } + + /// Configure MoEngage SDK Logs + /// [logLevel] - [LogLevel] for SDK logs + /// [isEnabledForReleaseBuild] If true, logs will be printed for the Release build. By default the logs are disabled for the Release build. + void configureLogs(LogLevel logLevel, + {bool isEnabledForReleaseBuild = false}) { + Logger.configureLogs(logLevel, isEnabledForReleaseBuild); + } + + /// Updates the number of the times Notification permission is requested + /// Note: This API is only applicable for Android Platform. This should not called in App/Widget lifecycle methods. + /// [requestCount] This count will be incremented to existing value + void updatePushPermissionRequestCountAndroid(int requestCount) { + _platform.updatePushPermissionRequestCountAndroid(requestCount, appId); + } + + /// Enable Device-id tracking. It is enabled by default, and should be called only if tracking is disabled at some point. + /// Note: This API is only for Android Platform + void enableDeviceIdTracking() { + _platform.updateDeviceIdentifierTrackingStatus(appId, keyDeviceId, true); + } + + /// Disables Device-id tracking + /// Note: This API is only for Android Platform + void disableDeviceIdTracking() { + _platform.updateDeviceIdentifierTrackingStatus(appId, keyDeviceId, false); + } + + /// Delete Current User Data From MoEngage Server + /// Note: This API is only applicable for Android Platform + /// @returns - Instance of [Future] of type [UserDeletionData] + /// @since 6.1.0 + Future deleteUser() { + return _platform.deleteUser(appId); + } +} diff --git a/flutter-sample/core/moengage_flutter/pubspec.yaml b/flutter-sample/core/moengage_flutter/pubspec.yaml new file mode 100644 index 00000000..8b86cf21 --- /dev/null +++ b/flutter-sample/core/moengage_flutter/pubspec.yaml @@ -0,0 +1,34 @@ +name: moengage_flutter +description: Flutter Plugin for MoEngage Platform. MoEngage is an Intelligent Customer Engagement Platform +version: 6.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + assets: + - config.json + plugin: + platforms: + android: + default_package: moengage_flutter_android + ios: + default_package: moengage_flutter_ios + web: + default_package: moengage_flutter_web + +dependencies: + flutter: + sdk: flutter + moengage_flutter_android: ^1.1.0 + moengage_flutter_ios: ^1.1.0 + moengage_flutter_platform_interface: ^1.1.0 + moengage_flutter_web: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.3.0 + plugin_platform_interface: ^2.0.0 diff --git a/flutter-sample/core/moengage_flutter/pubspec_overrides.yaml b/flutter-sample/core/moengage_flutter/pubspec_overrides.yaml new file mode 100644 index 00000000..7ff00db0 --- /dev/null +++ b/flutter-sample/core/moengage_flutter/pubspec_overrides.yaml @@ -0,0 +1,7 @@ +dependency_overrides: + moengage_flutter_platform_interface: + path: ../moengage_flutter_platform_interface/ + moengage_flutter_ios: + path: ../moengage_flutter_ios/ + moengage_flutter_android: + path: ../moengage_flutter_android/ \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter/test/data_provider/data_provider.dart b/flutter-sample/core/moengage_flutter/test/data_provider/data_provider.dart new file mode 100644 index 00000000..7cabf01e --- /dev/null +++ b/flutter-sample/core/moengage_flutter/test/data_provider/data_provider.dart @@ -0,0 +1,19 @@ +const num number = 10.56; + +const setUserAttributesData = { + 'bool': true, + 'int': 10, + 'double': 10.5, + 'num': number, + 'string': 'data', + 'int-array': [1, 2, 3, 4], + 'double-array': [1.0, 1.5, 2.456], + 'num-array': [1.04, 1, 2.456], + 'string-array': ['data', 'array'] +}; + +const unsetUserAttributesData = { + 'obj': {}, + 'bool-array': [true, false], + 'mixed-array': [1, 1.5, 'data'] +}; diff --git a/flutter-sample/core/moengage_flutter/test/mock_platform.dart b/flutter-sample/core/moengage_flutter/test/mock_platform.dart new file mode 100644 index 00000000..ca3daefa --- /dev/null +++ b/flutter-sample/core/moengage_flutter/test/mock_platform.dart @@ -0,0 +1,24 @@ +import 'package:moengage_flutter_platform_interface/src/internal/method_channel_moengage_flutter.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// Mock Platform Interface. +class MockMoEngageFlutterPlatform extends MethodChannelMoEngageFlutter + with MockPlatformInterfaceMixin { + String? setUserAttributeLastUserAttributeName; + dynamic setUserAttributeLastUserAttributeValue; + String? setUserAttributeLastAppId; + + @override + void setUserAttribute( + String userAttributeName, userAttributeValue, String appId) { + setUserAttributeLastUserAttributeName = userAttributeName; + setUserAttributeLastUserAttributeValue = userAttributeValue; + setUserAttributeLastAppId = appId; + } + + void clear() { + setUserAttributeLastUserAttributeName = null; + setUserAttributeLastUserAttributeValue = null; + setUserAttributeLastAppId = null; + } +} diff --git a/flutter-sample/core/moengage_flutter/test/moengage_flutter_test.dart b/flutter-sample/core/moengage_flutter/test/moengage_flutter_test.dart new file mode 100644 index 00000000..eb040c97 --- /dev/null +++ b/flutter-sample/core/moengage_flutter/test/moengage_flutter_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:moengage_flutter/moengage_flutter.dart'; + +import 'data_provider/data_provider.dart'; +import 'mock_platform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final MockMoEngageFlutterPlatform mock = MockMoEngageFlutterPlatform(); + MoEngageFlutterPlatform.instance = mock; + + tearDown(() => {mock.clear()}); + + test('User Attributes set', () async { + final platform = MoEngageFlutter('DAO6UGZ73D9RTK8B5W96TPYN'); + for (final entry in setUserAttributesData.entries) { + platform.setUserAttribute(entry.key, entry.value); + expect(mock.setUserAttributeLastUserAttributeName, entry.key); + expect(mock.setUserAttributeLastUserAttributeValue, entry.value); + } + }); + + test('User Attributes not set', () async { + final platform = MoEngageFlutter('DAO6UGZ73D9RTK8B5W96TPYN'); + for (final entry in unsetUserAttributesData.entries) { + platform.setUserAttribute(entry.key, entry.value); + expect(mock.setUserAttributeLastUserAttributeName, null); + expect(mock.setUserAttributeLastUserAttributeValue, null); + } + }); +} diff --git a/flutter-sample/core/moengage_flutter_android/CHANGELOG.md b/flutter-sample/core/moengage_flutter_android/CHANGELOG.md new file mode 100644 index 00000000..4898549a --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/CHANGELOG.md @@ -0,0 +1,15 @@ +# MoEngage Flutter Android Plugin + +# 07-12-2023 + +## 1.1.0 +- Fix: Android to Dart Method Channel Communication Breaking when `FirebaseMessaging.onBackgroundMessage` is used +- Google Policy - Delete User details API +- Add support for AGP `8.0.2` and above +- Upgrade Kotlin Version to `1.7.10` +- Support for `moe-android-sdk` version `12.10.01` and above + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/LICENSE b/flutter-sample/core/moengage_flutter_android/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/README.md b/flutter-sample/core/moengage_flutter_android/README.md new file mode 100644 index 00000000..edd4b0df --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/README.md @@ -0,0 +1,15 @@ +# moengage\_flutter\_android + +The Android implementation of [`moengage_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_flutter` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/android/.gitignore b/flutter-sample/core/moengage_flutter_android/android/.gitignore new file mode 100644 index 00000000..c6cbe562 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/flutter-sample/core/moengage_flutter_android/android/.project b/flutter-sample/core/moengage_flutter_android/android/.project new file mode 100644 index 00000000..d765ac77 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/.project @@ -0,0 +1,23 @@ + + + moengage_flutter + Project moengage_flutter created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/flutter-sample/core/moengage_flutter_android/android/build.gradle b/flutter-sample/core/moengage_flutter_android/android/build.gradle new file mode 100644 index 00000000..d680fb3e --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/build.gradle @@ -0,0 +1,58 @@ +group 'com.moengage.flutter' +version '1.0' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + jcenter() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.0.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdk 33 + namespace "com.moengage.flutter" + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + lintOptions { + disable 'InvalidPackage' + } + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = '1.8' + } +} +dependencies { + compileOnly("com.moengage:moe-android-sdk:12.10.01") + compileOnly("com.moengage:inapp:7.1.0") + api("com.moengage:plugin-base:3.4.0") + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} +apply from: file("./user-agent.gradle") diff --git a/flutter-sample/core/moengage_flutter_android/android/gradle.properties b/flutter-sample/core/moengage_flutter_android/android/gradle.properties new file mode 100644 index 00000000..d7f5c23b --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M +android.defaults.buildfeatures.buildconfig=true diff --git a/flutter-sample/core/moengage_flutter_android/android/gradle/wrapper/gradle-wrapper.properties b/flutter-sample/core/moengage_flutter_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..572fb576 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Feb 11 14:35:02 IST 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/android/settings.gradle b/flutter-sample/core/moengage_flutter_android/android/settings.gradle new file mode 100644 index 00000000..daf89ebd --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'moengage_flutter' diff --git a/flutter-sample/core/moengage_flutter_android/android/src/main/AndroidManifest.xml b/flutter-sample/core/moengage_flutter_android/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fabd01f4 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/Constants.kt b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/Constants.kt new file mode 100644 index 00000000..9eddb436 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/Constants.kt @@ -0,0 +1,50 @@ +package com.moengage.flutter + +import android.os.Build.VERSION + +/** + * @author Umang Chamaria + * Date: 2019-12-12 + */ + +const val MODULE_TAG = "MoEFlutter_" +const val INTEGRATION_TYPE = "flutter" +const val FLUTTER_PLUGIN_CHANNEL_NAME = "com.moengage/core" + +//Method name constants +const val METHOD_NAME_INITIALISE = "initialise" +const val METHOD_NAME_TRACK_EVENT = "trackEvent" +const val METHOD_NAME_SET_USER_ATTRIBUTE = "setUserAttribute" +const val METHOD_NAME_SET_ALIAS = "setAlias" +const val METHOD_NAME_SET_USER_ATTRIBUTE_LOCATION = "setUserAttributeLocation" +const val METHOD_NAME_SET_USER_ATTRIBUTE_TIMESTAMP = "setUserAttributeTimestamp" +const val METHOD_NAME_SET_APP_STATUS = "setAppStatus" +const val METHOD_NAME_SHOW_IN_APP = "showInApp" +const val METHOD_NAME_LOGOUT = "logout" +const val METHOD_NAME_PUSH_TOKEN = "pushToken" +const val METHOD_NAME_PUSH_PAYLOAD = "pushPayload" +const val METHOD_NAME_SELF_HANDLED_INAPP = "selfHandledInApp" +const val METHOD_NAME_SET_APP_CONTEXT = "setAppContext" +const val METHOD_NAME_RESET_APP_CONTEXT = "resetCurrentContext" +const val METHOD_NAME_OPT_OUT_TRACKING = "optOutTracking" +const val METHOD_NAME_SELF_HANDLED_CALLBACK = "selfHandledCallback" +const val METHOD_NAME_UPDATE_SDK_STATE = "updateSdkState" +const val METHOD_NAME_ON_ORIENTATION_CHANGED = "onOrientationChanged" +const val METHOD_NAME_UPDATE_DEVICE_IDENTIFIER_TRACKING_STATUS = + "updateDeviceIdentifierTrackingStatus" +const val METHOD_NAME_SETUP_NOTIFICATION_CHANNEL = "setupNotificationChannels" +const val METHOD_NAME_NAVIGATE_TO_SETTINGS = "navigateToSettings" +const val METHOD_NAME_REQUEST_PUSH_PERMISSION = "requestPushPermission" +const val METHOD_NAME_PERMISSION_RESPONSE = "permissionResponse" + +const val KEY_TYPE = "type" + +const val METHOD_NAME_PUSH_PERMISSION_PERMISSION_COUNT = "updatePushPermissionRequestCount" + +// Asset Location for config.json under moengage_flutter package. +const val ASSET_CONFIG_FILE_PATH = "flutter_assets/packages/moengage_flutter/config.json" +const val VERSION_KEY = "version" + +// Delete User +const val METHOD_NAME_DELETE_USER = "deleteUser" +const val ERROR_CODE_DELETE_USER = "DELETE_USER_ERROR" \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/EventEmitterImpl.kt b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/EventEmitterImpl.kt new file mode 100644 index 00000000..450ef9bb --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/EventEmitterImpl.kt @@ -0,0 +1,155 @@ +package com.moengage.flutter + +import com.moengage.core.LogLevel +import com.moengage.core.internal.logger.Logger +import com.moengage.plugin.base.internal.EventEmitter +import com.moengage.plugin.base.internal.clickDataToJson +import com.moengage.plugin.base.internal.inAppDataToJson +import com.moengage.plugin.base.internal.model.events.Event +import com.moengage.plugin.base.internal.model.events.EventType +import com.moengage.plugin.base.internal.model.events.inapp.InAppActionEvent +import com.moengage.plugin.base.internal.model.events.inapp.InAppLifecycleEvent +import com.moengage.plugin.base.internal.model.events.inapp.InAppSelfHandledEvent +import com.moengage.plugin.base.internal.model.events.push.PermissionEvent +import com.moengage.plugin.base.internal.model.events.push.PushClickedEvent +import com.moengage.plugin.base.internal.model.events.push.TokenEvent +import com.moengage.plugin.base.internal.permissionResultToJson +import com.moengage.plugin.base.internal.pushPayloadToJson +import com.moengage.plugin.base.internal.selfHandledDataToJson +import com.moengage.plugin.base.internal.tokenEventToJson +import org.json.JSONObject +import java.util.* + + +/** + * @author Arshiya Khanum + * Date: 2020/10/21 + */ +class EventEmitterImpl(private val onEvent: (methodName: String, payload: String) -> Unit) : + EventEmitter { + + private val tag: String = "${MODULE_TAG}EventEmitterImpl" + + override fun emit(event: Event) { + try { + Logger.print { "$tag emit() : event: $event" } + when (event) { + is InAppActionEvent -> { + this.emitInAppActionEvent(event) + } + is InAppLifecycleEvent -> { + this.emitInAppLifeCycleEvent(event) + } + is InAppSelfHandledEvent -> { + this.emitInAppSelfHandledEvent(event) + } + is PushClickedEvent -> { + emitPushEvent(event) + } + is TokenEvent -> { + emitPushTokenEvent(event) + } + is PermissionEvent -> { + emitPermissionEvent(event) + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emit() : " } + } + } + + private fun emitInAppActionEvent(inAppActionEvent: InAppActionEvent) { + try { + Logger.print { "$tag emitInAppActionEvent() : inAppActionEvent: ${inAppActionEvent + .eventType} , ${inAppActionEvent.clickData}" } + val eventType = eventMap[inAppActionEvent.eventType] ?: return + val campaign: JSONObject = clickDataToJson(inAppActionEvent.clickData) + emit(eventType, campaign) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitInAppActionEvent() : " } + } + } + + private fun emitInAppLifeCycleEvent(inAppLifecycleEvent: InAppLifecycleEvent) { + try { + Logger.print { "$tag emitInAppLifeCycleEvent() : inAppLifecycleEvent: $inAppLifecycleEvent" } + val eventType = eventMap[inAppLifecycleEvent.eventType] ?: return + val campaign = inAppDataToJson(inAppLifecycleEvent.inAppData) + emit(eventType, campaign) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitInAppLifeCycleEvent() : " } + } + } + + private fun emitInAppSelfHandledEvent(inAppSelfHandledEvent: InAppSelfHandledEvent) { + try { + Logger.print { "$tag emitInAppSelfHandledEvent() : inAppSelfHandledEvent: " + + "${inAppSelfHandledEvent.data}" } + val eventType = eventMap[inAppSelfHandledEvent.eventType] + ?: return + val campaign: JSONObject = + selfHandledDataToJson(inAppSelfHandledEvent.data) + emit(eventType, campaign) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitInAppSelfHandledEvent() : " } + } + } + + private fun emitPushEvent(pushEvent: PushClickedEvent) { + try { + Logger.print { "$tag emitPushEvent() : pushEvent: $pushEvent" } + val eventType = eventMap[pushEvent.eventType] ?: return + val payload = pushPayloadToJson(pushEvent.payload) + emit(eventType, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitPushEvent() : " } + } + } + + private fun emitPushTokenEvent(tokenEvent: TokenEvent) { + try { + Logger.print { "$tag emitPushTokenEvent() : tokenEvent: $tokenEvent" } + val eventType = eventMap[tokenEvent.eventType] ?: return + val payload = tokenEventToJson(tokenEvent) + emit(eventType, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitPushTokenEvent() : " } + } + } + + private fun emit(methodName: String, payload: JSONObject) { + try { + Logger.print { "$tag emit() : methodName: $methodName , payload: $payload" } + onEvent(methodName, payload.toString()) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emit() : " } + } + } + + private fun emitPermissionEvent(event: PermissionEvent) { + try { + Logger.print { "$tag emitPermissionEvent() permission event: $event:" } + val eventType = eventMap[event.eventType] ?: return + val payload = permissionResultToJson(event.result) + emit(eventType, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag emitPermissionEvent() : " } + } + } + + companion object { + + private val eventMap = EnumMap(EventType::class.java) + + init { + eventMap[EventType.PUSH_CLICKED] = "onPushClick" + eventMap[EventType.INAPP_SHOWN] = "onInAppShown" + eventMap[EventType.INAPP_NAVIGATION] = "onInAppClick" + eventMap[EventType.INAPP_CLOSED] = "onInAppDismiss" + eventMap[EventType.INAPP_CUSTOM_ACTION] = "onInAppCustomAction" + eventMap[EventType.INAPP_SELF_HANDLED_AVAILABLE] = "onInAppSelfHandle" + eventMap[EventType.PUSH_TOKEN_GENERATED] = "onPushTokenGenerated" + eventMap[EventType.PERMISSION] = "onPermissionResult" + } + } +} diff --git a/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/GlobalCache.kt b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/GlobalCache.kt new file mode 100644 index 00000000..cd7928be --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/GlobalCache.kt @@ -0,0 +1,8 @@ +package com.moengage.flutter + +internal object GlobalCache { + + //Flag to Enable Queuing of events on App Background and on next App Open, the events will be flushed. + var lifecycleAwareCallbackEnabled: Boolean = false + +} \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEFlutterHelper.kt b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEFlutterHelper.kt new file mode 100644 index 00000000..5c956ee8 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEFlutterHelper.kt @@ -0,0 +1,36 @@ +package com.moengage.flutter + +import com.moengage.core.internal.inapp.InAppManager +import com.moengage.core.internal.logger.Logger +import com.moengage.inapp.MoEInAppHelper + +/** + * @author Arshiya Khanum + */ +public class MoEFlutterHelper { + + private val tag = "${MODULE_TAG}MoEFlutterHelper" + + public companion object { + + private var instance: MoEFlutterHelper? = null + + @JvmStatic + public fun getInstance(): MoEFlutterHelper { + return instance ?: synchronized(MoEFlutterHelper::class.java) { + val inst = instance ?: MoEFlutterHelper() + instance = inst + inst + } + } + } + + public fun onConfigurationChanged() { + Logger.print { "$tag onConfigurationChanged() : " } + if (!InAppManager.hasModule()) { + Logger.print { "$tag onConfigurationChanged() : InApp module not found." } + return + } + MoEInAppHelper.getInstance().onConfigurationChanged() + } +} \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEInitializer.kt b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEInitializer.kt new file mode 100644 index 00000000..d8bb3bba --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEInitializer.kt @@ -0,0 +1,102 @@ +package com.moengage.flutter + +import android.content.Context +import com.moengage.core.LogLevel +import com.moengage.core.MoEngage +import com.moengage.core.internal.logger.Logger +import com.moengage.core.internal.model.IntegrationMeta +import com.moengage.core.model.SdkState +import com.moengage.plugin.base.internal.PluginInitializer +import org.json.JSONObject + +/** + * @author Umang Chamaria + * Date: 2019-12-03 + */ +class MoEInitializer { + companion object { + private const val tag: String = "${MODULE_TAG}MoEInitializer" + + /** + * Initialise the default instance of SDK with configuration provided in [MoEngage.Builder] + * + * @param context Context + * @param builder Instance of [MoEngage.Builder] + * @param lifecycleAwareCallbackEnabled - If true, on App background the events will be queued + * and on App Open the events will be flushed. + */ + @JvmStatic + @JvmOverloads + fun initialiseDefaultInstance( + context: Context, + builder: MoEngage.Builder, + lifecycleAwareCallbackEnabled: Boolean = false + ) { + try { + Logger.print { "$tag initialiseDefaultInstance() : Will try to initialize the sdk." } + PluginInitializer.initialize( + builder, + IntegrationMeta( + INTEGRATION_TYPE, + getMoEngageFlutterVersion(context) + ), + SdkState.ENABLED + ) + GlobalCache.lifecycleAwareCallbackEnabled = lifecycleAwareCallbackEnabled + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag initialiseDefaultInstance() : " } + } + } + + /** + * Initialise the default instance of SDK with configuration provided in [MoEngage.Builder] and + * SDK state, i.e. whether the SDK should be in enabled or disabled state. + * + * By default the SDK is enabled. Use this API only if you have a requirement to + * enable/disable SDK, else use [MoEngage.initialiseDefaultInstance]. + * + * **Note:** State is persisted across session, once the SDK is disabled it will remain + * in disabled state until enabled again. + * + * @param context Context + * @param builder Instance of [MoEngage.Builder] + * @param sdkState [SdkState] + * @param lifecycleAwareCallbackEnabled - If true, on App background the events will be queued + * and on App Open the events will be flushed. + */ + @JvmStatic + @JvmOverloads + fun initialiseDefaultInstance( + context: Context, + builder: MoEngage.Builder, + sdkState: SdkState, + lifecycleAwareCallbackEnabled: Boolean = false + ) { + try { + Logger.print { "$tag initialiseDefaultInstance() : Will try to initialize the sdk." } + PluginInitializer.initialize( + builder, + IntegrationMeta(INTEGRATION_TYPE, getMoEngageFlutterVersion(context)), + sdkState + ) + GlobalCache.lifecycleAwareCallbackEnabled = lifecycleAwareCallbackEnabled + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag initialiseDefaultInstance() : " } + } + } + + /** + * Get moengage_flutter version from Config File + */ + private fun getMoEngageFlutterVersion(context: Context): String { + return try { + val json = context.assets.open(ASSET_CONFIG_FILE_PATH) + .bufferedReader().use { it.readText() } + JSONObject(json).getString(VERSION_KEY) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getMoEngageFlutterVersion() : " } + "" + } + } + } +} \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEngageFlutterPlugin.kt b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEngageFlutterPlugin.kt new file mode 100644 index 00000000..52856a1b --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/src/main/kotlin/com/moengage/flutter/MoEngageFlutterPlugin.kt @@ -0,0 +1,457 @@ +package com.moengage.flutter + +import android.content.Context +import android.os.Handler +import android.os.Looper +import com.moengage.core.LogLevel +import com.moengage.core.MoECoreHelper +import com.moengage.core.internal.logger.Logger +import com.moengage.core.listeners.AppBackgroundListener +import com.moengage.plugin.base.internal.PluginHelper +import com.moengage.plugin.base.internal.setEventEmitter +import com.moengage.plugin.base.internal.userDeletionDataToJson +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +class MoEngageFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { + + private val tag = "${MODULE_TAG}MoEngageFlutterPlugin" + private lateinit var context: Context + private val pluginHelper = PluginHelper() + + private val appBackgroundListener = AppBackgroundListener { _, _ -> + run { + Logger.print { "$tag onAppBackground() : Detaching the Framework" } + pluginHelper.onFrameworkDetached() + } + } + + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + Logger.print { "$tag onAttachedToEngine() : Registering MoEngageFlutterPlugin" } + context = binding.applicationContext + flutterPluginBinding = binding + if (methodChannel == null) { + initPlugin(binding.binaryMessenger) + } + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + try { + Logger.print { "$tag onDetachedFromEngine() : Registering MoEngageFlutterPlugin" } + pluginHelper.onFrameworkDetached() + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag onDetachedFromEngine() " } + } + } + + private fun initPlugin(binaryMessenger: BinaryMessenger) { + try { + Logger.print { "$tag initPlugin(): Initializing MoEngage Flutter Plugin" } + methodChannel = MethodChannel(binaryMessenger, FLUTTER_PLUGIN_CHANNEL_NAME) + methodChannel?.setMethodCallHandler(this) + setEventEmitter(EventEmitterImpl(::sendCallback)) + if (GlobalCache.lifecycleAwareCallbackEnabled) { + Logger.print { "$tag initPlugin() Adding App Background Listener: " } + MoECoreHelper.addAppBackgroundListener(appBackgroundListener) + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag initPlugin() : " } + } + } + + private fun sendCallback(methodName: String, message: String) { + try { + Handler(Looper.getMainLooper()).post { + try { + methodChannel?.invokeMethod(methodName, message) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag sendCallback() " } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag sendCallback() : " } + } + } + + @Suppress("SENSELESS_COMPARISON") + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + try { + if (call == null) { + Logger.print(LogLevel.ERROR) { "$tag onMethodCall() : MethodCall instance is null cannot proceed further." } + return + } + if (context == null) { + Logger.print(LogLevel.ERROR) { + "$tag onMethodCall() : Context is null cannot " + + "proceed further." + } + return + } + Logger.print { "$tag onMethodCall() : method: ${call.method}" } + when (call.method) { + METHOD_NAME_INITIALISE -> onInitialised(call) + METHOD_NAME_SET_USER_ATTRIBUTE -> setUserAttribute(call) + METHOD_NAME_SET_USER_ATTRIBUTE_LOCATION -> setUserLocation(call) + METHOD_NAME_TRACK_EVENT -> trackEvent(call) + METHOD_NAME_SHOW_IN_APP -> showInApp(call) + METHOD_NAME_LOGOUT -> logout(call) + METHOD_NAME_SET_ALIAS -> setAlias(call) + METHOD_NAME_SET_APP_STATUS -> setAppStatus(call) + METHOD_NAME_SET_USER_ATTRIBUTE_TIMESTAMP -> setTimestamp(call) + METHOD_NAME_SELF_HANDLED_INAPP -> getSelfHandledInApp(call) + METHOD_NAME_SET_APP_CONTEXT -> setAppContext(call) + METHOD_NAME_RESET_APP_CONTEXT -> resetAppContext(call) + METHOD_NAME_PUSH_PAYLOAD -> passPushPayload(call) + METHOD_NAME_PUSH_TOKEN -> passPushToken(call) + METHOD_NAME_OPT_OUT_TRACKING -> optOutTracking(call) + METHOD_NAME_SELF_HANDLED_CALLBACK -> selfHandledCallback(call) + METHOD_NAME_UPDATE_SDK_STATE -> updateSdkState(call) + METHOD_NAME_ON_ORIENTATION_CHANGED -> onOrientationChanged() + METHOD_NAME_UPDATE_DEVICE_IDENTIFIER_TRACKING_STATUS -> + updateDeviceIdentifierTrackingStatus(call) + METHOD_NAME_SETUP_NOTIFICATION_CHANNEL -> setupNotificationChannels() + METHOD_NAME_NAVIGATE_TO_SETTINGS -> navigateToSettings() + METHOD_NAME_REQUEST_PUSH_PERMISSION -> requestPushPermission() + METHOD_NAME_PERMISSION_RESPONSE -> permissionResponse(call) + METHOD_NAME_PUSH_PERMISSION_PERMISSION_COUNT -> + updatePushPermissionRequestCount(call) + METHOD_NAME_DELETE_USER -> deleteUser(call, result) + else -> Logger.print(LogLevel.ERROR) { + "$tag onMethodCall() : No mapping for this" + + " method." + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag onMethodCall() : " } + } + } + + private fun logout(methodCall: MethodCall) { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag logout() : Arguments: $payload" } + pluginHelper.logout(context, payload) + } + + private fun showInApp(methodCall: MethodCall) { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag showInApp() : Arguments: $payload" } + pluginHelper.showInApp(context, payload) + } + + private fun onInitialised(methodCall: MethodCall) { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + pluginHelper.initialise(payload) + Logger.print { "$tag onInitialised() : MoEngage Flutter plugin initialised." } + } + + private fun setUserAttribute(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag setUserAttribute() : Arguments: $payload" } + pluginHelper.setUserAttribute(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag setUserAttribute() : " } + } + } + + private fun setUserLocation(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag setUserLocation() : Argument: $payload" } + pluginHelper.setUserAttribute(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag setUserLocation() : " } + } + } + + private fun trackEvent(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) { + Logger.print(LogLevel.ERROR) { + "$tag trackEvent() : Arguments are null, cannot" + + " trackEvent" + } + return + } + val payload = methodCall.arguments as String + Logger.print { "$tag trackEvent() : Argument :$payload" } + pluginHelper.trackEvent(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag trackEvent() : " } + } + } + + private fun setAlias(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag setAlias() : Argument :$payload" } + pluginHelper.setAlias(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag setAlias() : " } + } + } + + private fun setAppStatus(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag setAppStatus() : Arguments :$payload" } + pluginHelper.setAppStatus(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag setAppStatus() : " } + } + } + + private fun setTimestamp(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag setTimestamp() : Arguments: $payload" } + pluginHelper.setUserAttribute(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag setTimestamp() : " } + } + } + + private fun getSelfHandledInApp(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag getSelfHandledInApp() : Arguments: $payload" } + pluginHelper.getSelfHandledInApp(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getSelfHandledInApp() : " } + } + } + + private fun setAppContext(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag setAppContext() : Arguments: $payload" } + pluginHelper.setAppContext(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag setAppContext() : " } + } + } + + private fun resetAppContext(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag resetAppContext() : Arguments: $payload" } + pluginHelper.resetAppContext(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag resetAppContext() : " } + } + } + + private fun passPushToken(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag passPushToken() : Arguments: $payload" } + pluginHelper.passPushToken(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag passPushToken() : " } + } + } + + private fun passPushPayload(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag passPushPayload() : Arguments: $payload" } + pluginHelper.passPushPayload(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag passPushPayload() : " } + } + } + + private fun optOutTracking(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag optOutTracking() : Arguments: $payload" } + pluginHelper.optOutTracking(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag optOutTracking() : " } + } + } + + private fun selfHandledCallback(methodCall: MethodCall) { + try { + Logger.print { "$tag selfHandledCallback() : Arguments: $methodCall" } + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag selfHandledCallback() : Arguments: $payload" } + pluginHelper.selfHandledCallback(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag selfHandledCallback() : " } + } + } + + private fun updateSdkState(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag updateSdkState() : Arguments: $payload" } + pluginHelper.storeFeatureStatus(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag updateSdkState() : " } + } + } + + private fun onOrientationChanged() { + Logger.print { "$tag onOrientationChanged() : " } + pluginHelper.onConfigurationChanged() + } + + private fun updateDeviceIdentifierTrackingStatus(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag updateDeviceIdentifierTrackingStatus() : Arguments: $payload" } + pluginHelper.deviceIdentifierTrackingStatusUpdate(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag updateDeviceIdentifierTrackingStatus() : " } + } + } + + private fun setupNotificationChannels() { + try { + pluginHelper.setUpNotificationChannels(context) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag setupNotificationChannel() :" } + } + } + + private fun navigateToSettings() { + try { + pluginHelper.navigateToSettings(context) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag navigateToSettings() :" } + } + } + + private fun requestPushPermission() { + try { + pluginHelper.requestPushPermission(context) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag requestPushPermission() :" } + } + } + + private fun permissionResponse(methodCall: MethodCall) { + try { + Logger.print { "$tag permissionResponse() : Arguments: ${methodCall.arguments}" } + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag permissionResponse() : Payload: $payload" } + pluginHelper.permissionResponse(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag permissionResponse() :" } + } + } + + private fun updatePushPermissionRequestCount(methodCall: MethodCall) { + try { + Logger.print { "$tag updatePushPermissionRequestCount() : Arguments: ${methodCall.arguments}" } + if (methodCall.arguments == null) return + val payload: String = methodCall.arguments.toString() + Logger.print { "$tag updatePushPermissionRequestCount() : Payload: $payload" } + pluginHelper.updatePushPermissionRequestCount(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag updatePushPermissionRequestCount() :" } + } + } + + /** + * API to delete the user from MoEngage Server + * @param methodCall - Instance of [MethodCall] to get message from Flutter Method Channel + * @param result - Instance of [MethodChannel.Result] to send result to Flutter Method Channel + * @since 1.1.0 + */ + private fun deleteUser(methodCall: MethodCall, result: MethodChannel.Result) { + try { + Logger.print { "$tag deleteUser() : Arguments: ${methodCall.arguments}" } + if (methodCall.arguments == null) { + result.error(ERROR_CODE_DELETE_USER, "Invalid Arguments", null) + return + } + val payload = methodCall.arguments.toString() + Logger.print { "$tag updatePushPermissionRequestCount() : Payload: $payload" } + pluginHelper.deleteUser(context, payload) { data -> + result.success(userDeletionDataToJson(data).toString()) + } + } catch (t: Throwable) { + result.error(ERROR_CODE_DELETE_USER, "Error occured while Deleting the User", null) + Logger.print(LogLevel.ERROR, t) { "deleteUser(): " } + } + } + + + /** + * Called when the plugin is attached to Flutter Activity. + * @param binding instance of [ActivityPluginBinding] + */ + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + Logger.print { "$tag onAttachedToActivity() : Attached To Activity" } + flutterPluginBinding?.binaryMessenger?.let { + initPlugin(it) + } + } + + + /** + * Called when the plugin is Detached From Flutter Activity. + */ + override fun onDetachedFromActivity() { + Logger.print { "$tag onDetachedFromActivity() : Resetting methodChannel to `null`" } + methodChannel = null + } + + /** + * Called when the plugin is Detached From Flutter Activity for Config Changes + */ + override fun onDetachedFromActivityForConfigChanges() { + Logger.print { + "$tag onDetachedFromActivityForConfigChanges() : Detached From Activity for Config changes" + } + } + + /** + * Called when the plugin is Reattached to Flutter Activity For Config Changes. + * @param binding instance of [ActivityPluginBinding] + */ + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + Logger.print { + "$tag onReattachedToActivityForConfigChanges() : ReAttached To Activity for Config changes" + } + } + + + companion object { + /** + * Static MethodChannel instance to avoid plugin reinitializing from Background Isolate + */ + internal var methodChannel: MethodChannel? = null + + /** + * Instance of [FlutterPluginBinding] to reinitialize the Method Channel on [onAttachedToActivity] + */ + internal var flutterPluginBinding: FlutterPluginBinding? = null + } +} \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/android/user-agent.gradle b/flutter-sample/core/moengage_flutter_android/android/user-agent.gradle new file mode 100644 index 00000000..14c13f62 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/android/user-agent.gradle @@ -0,0 +1,19 @@ +import java.util.regex.Matcher +import java.util.regex.Pattern + +String libraryVersionName = "UNKNOWN" +File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml') + +if (pubspec.exists()) { + String yaml = pubspec.text + // Using \s*['|"]?([^\n|'|"]*)['|"]? to extract version number. + Matcher versionMatcher = Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", Pattern.MULTILINE).matcher(yaml) + if (versionMatcher.find()) libraryVersionName = versionMatcher.group(1).replaceAll("\\+", "-") +} + +android { + defaultConfig { + // BuildConfig.VERSION_NAME + buildConfigField("String", 'MOENGAGE_FLUTTER_LIBRARY_VERSION', "\"${libraryVersionName}\"") + } +} diff --git a/flutter-sample/core/moengage_flutter_android/lib/moengage_flutter_android.dart b/flutter-sample/core/moengage_flutter_android/lib/moengage_flutter_android.dart new file mode 100644 index 00000000..45f2be5c --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/lib/moengage_flutter_android.dart @@ -0,0 +1,310 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; +import 'src/internal/utils/payload_mapper.dart'; + +/// The Android implementation of [MoEngageFlutterPlatform]. +class MoEngageFlutterAndroid extends MoEngageFlutterPlatform { + /// The method channel used to interact with the native platform. + final MethodChannel _methodChannel = const MethodChannel(channelName); + + /// Log Tag + final String tag = '${TAG}MoEngageFlutterAndroid'; + + /// Registers this class as the default instance of [MoEngageFlutterPlatform] + static void registerWith() { + Logger.v('Registering MoEngageFlutterAndroid with Platform Interface'); + MoEngageFlutterPlatform.instance = MoEngageFlutterAndroid(); + } + + @override + void initialise(MoEInitConfig moEInitConfig, String appId) { + _methodChannel.invokeMethod(methodInitialise, + InitConfigPayloadMapper().getInitPayload(appId, moEInitConfig)); + } + + @override + void trackEvent( + String eventName, MoEProperties eventAttributes, String appId) { + _methodChannel.invokeMethod(methodTrackEvent, + json.encode(getEventPayload(eventName, eventAttributes, appId))); + } + + @override + void setUniqueId(String uniqueId, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameUniqueId, userAttrTypeGeneral, uniqueId, appId)); + } + + @override + void setAlias(String newUniqueId, String appId) { + _methodChannel.invokeMethod( + methodSetAlias, json.encode(getAliasPayload(newUniqueId, appId))); + } + + @override + void setUserName(String userName, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameUserName, userAttrTypeGeneral, userName, appId)); + } + + @override + void setFirstName(String firstName, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameFirstName, userAttrTypeGeneral, firstName, appId)); + } + + @override + void setLastName(String lastName, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameLastName, userAttrTypeGeneral, lastName, appId)); + } + + @override + void setEmail(String emailId, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameEmailId, userAttrTypeGeneral, emailId, appId)); + } + + @override + void setPhoneNumber(String phoneNumber, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNamePhoneNum, userAttrTypeGeneral, phoneNumber, appId)); + } + + @override + void setGender(MoEGender gender, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson(userAttrNameGender, userAttrTypeGeneral, + genderToString(gender), appId)); + } + + @override + void setLocation(MoEGeoLocation location, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson(userAttrNameLocation, userAttrTypeLocation, + location.toMap(), appId)); + } + + @override + void setBirthDate(String birthDate, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameBirtdate, userAttrTypeTimestamp, birthDate, appId)); + } + + @override + void setUserAttribute( + String userAttributeName, dynamic userAttributeValue, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttributeName, userAttrTypeGeneral, userAttributeValue, appId)); + } + + @override + void setUserAttributeIsoDate( + String userAttributeName, String isoDateString, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttributeName, userAttrTypeTimestamp, isoDateString, appId)); + } + + @override + void setUserAttributeLocation( + String userAttributeName, MoEGeoLocation location, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttributeName, userAttrTypeLocation, location.toMap(), appId)); + } + + @override + void setAppStatus(MoEAppStatus appStatus, String appId) { + _methodChannel.invokeMethod( + methodSetAppStatus, json.encode(getAppStatusPayload(appStatus, appId))); + } + + @override + void showInApp(String appId) { + _methodChannel.invokeMethod( + methodShowInApp, json.encode(getAccountMeta(appId))); + } + + @override + void logout(String appId) { + _methodChannel.invokeMethod( + methodLogout, json.encode(getAccountMeta(appId))); + } + + @override + void getSelfHandledInApp(String appId) { + _methodChannel.invokeMethod( + methodSelfHandledInApp, json.encode(getAccountMeta(appId))); + } + + @override + void selfHandledCallback(Map payload) { + _methodChannel.invokeMethod( + methodSelfHandledCallback, json.encode(payload)); + } + + @override + void setCurrentContext(List contexts, String appId) { + _methodChannel.invokeMethod(methodSetAppContext, + json.encode(getInAppContextPayload(contexts, appId))); + } + + @override + void resetCurrentContext(String appId) { + _methodChannel.invokeMethod( + methodResetAppContext, json.encode(getAccountMeta(appId))); + } + + @override + void passPushToken( + String pushToken, MoEPushService pushService, String appId) { + _methodChannel.invokeMethod( + methodPushToken, _getPushTokenPayload(pushToken, pushService, appId)); + } + + @override + void optOutDataTracking(bool optOutDataTracking, String appId) { + _methodChannel.invokeMethod( + methodOptOutTracking, + json.encode(getOptOutTrackingPayload( + gdprOptOutTypeData, optOutDataTracking, appId))); + } + + @override + void registerForPushNotification() { + Logger.v('registerForPushNotification() method not available for Android'); + } + + @override + void passPushPayload( + Map payload, MoEPushService pushService, String appId) { + final String pushPayload = _getPushPayload(payload, pushService, appId); + _methodChannel.invokeMethod(methodPushPayLoad, pushPayload); + } + + @override + void updateSdkState(bool shouldEnableSdk, String appId) { + _methodChannel.invokeMethod(methodUpdateSdkState, + json.encode(getUpdateSdkStatePayload(shouldEnableSdk, appId))); + } + + @override + void onOrientationChanged() { + _methodChannel.invokeMethod(methodOnOrientationChanged); + } + + @override + void updateDeviceIdentifierTrackingStatus( + String appId, String identifierType, bool state) { + _methodChannel.invokeListMethod(methodUpdateDeviceIdentifierTrackingStatus, + _getDeviceIdentifierJson(appId, identifierType, state)); + } + + @override + void setupNotificationChannel() { + _methodChannel.invokeMethod(methodSetupNotificationChannelAndroid); + } + + @override + void permissionResponse(bool isGranted, PermissionType type) { + _methodChannel.invokeMethod(methodPermissionResponse, + json.encode(getPermissionResponsePayload(isGranted, type))); + } + + @override + void navigateToSettings() { + _methodChannel.invokeMethod(methodNavigateToSettingsAndroid); + } + + @override + void requestPushPermission() { + _methodChannel.invokeMethod(methodRequestPushPermissionAndroid); + } + + @override + void updatePushPermissionRequestCountAndroid(int requestCount, String appId) { + _methodChannel.invokeMethod(methodUpdatePushPermissionRequestCount, + _getUpdatePushCountJsonPayload(requestCount, appId)); + } + + /// Delete User Data from MoEngage Server + /// [appId] - MoEngage App ID + /// @returns - Instance of [Future] of type [UserDeletionData] + /// @since 1.1.0 + @override + Future deleteUser(String appId) async { + try { + final result = await _methodChannel.invokeMethod( + methodNameDeleteUser, + getAccountMeta(appId), + ); + return Future.value( + PayloadMapper().deSerializeDeleteUserData(result.toString(), appId)); + } catch (ex) { + Logger.e(' $tag deleteUser(): Error', error: ex); + return Future.error(ex); + } + } + + String _getUserAttributePayloadJson(String attributeName, + String attributeType, dynamic attributeValue, String appId) { + return json.encode(getUserAttributePayload( + attributeName, attributeType, attributeValue, appId)); + } + + String _getPushTokenPayload( + String pushToken, MoEPushService pushService, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = { + keyPushToken: pushToken, + keyService: pushService.asString + }; + return json.encode(payload); + } + + String _getPushPayload(Map pushPayload, + MoEPushService pushService, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = { + keyPayload: pushPayload, + keyService: pushService.asString + }; + return json.encode(payload); + } + + String _getDeviceIdentifierJson( + String appId, String identifierType, bool state) { + final Map payload = getAccountMeta(appId); + payload[keyData] = {identifierType: state}; + return json.encode(payload); + } + + String _getUpdatePushCountJsonPayload(int requestCount, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = {keyUpdatePushPermissionCount: requestCount}; + return jsonEncode(payload); + } +} diff --git a/flutter-sample/core/moengage_flutter_android/lib/src/internal/utils/payload_mapper.dart b/flutter-sample/core/moengage_flutter_android/lib/src/internal/utils/payload_mapper.dart new file mode 100644 index 00000000..59e06ad2 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/lib/src/internal/utils/payload_mapper.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; + +/// Payload Mapper Util Class to convert JSON Payloads to Model Class +/// @since 1.1.0 +class PayloadMapper { + /// Log tag for Payload Mapper + final tag = '${TAG}PayloadMapper'; + + /// Get [UserDeletionData] from Json String Payload + /// [data] - JSON String Payload + /// [appId] - MoEngage APP-Id + /// @since 1.1.0 + UserDeletionData deSerializeDeleteUserData(String data, String appId) { + try { + final payload = jsonDecode(data) as Map; + Logger.v('$tag deSerializeDeleteUserData(): $data'); + return UserDeletionData( + accountMeta: accountMetaFromMap( + payload[keyAccountMeta] as Map), + isSuccess: + (payload[keyData][keyUserDeletionStatus] ?? false) as bool); + } catch (ex) { + Logger.e(' $tag deSerializeDeleteUserData(): Parsing Error', error: ex); + return UserDeletionData( + accountMeta: AccountMeta(appId), isSuccess: false); + } + } +} diff --git a/flutter-sample/core/moengage_flutter_android/pubspec.yaml b/flutter-sample/core/moengage_flutter_android/pubspec.yaml new file mode 100644 index 00000000..2feeb3ed --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/pubspec.yaml @@ -0,0 +1,28 @@ +name: moengage_flutter_android +description: Android implementation of the moengage_flutter plugin +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_flutter + platforms: + android: + package: com.moengage.flutter + pluginClass: MoEngageFlutterPlugin + dartPluginClass: MoEngageFlutterAndroid + +dependencies: + flutter: + sdk: flutter + moengage_flutter_platform_interface: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/core/moengage_flutter_android/pubspec_overrides.yaml b/flutter-sample/core/moengage_flutter_android/pubspec_overrides.yaml new file mode 100644 index 00000000..1392d9f2 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + moengage_flutter_platform_interface: + path: ../moengage_flutter_platform_interface/ \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_android/test/comparator.dart b/flutter-sample/core/moengage_flutter_android/test/comparator.dart new file mode 100644 index 00000000..3e4315ee --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/test/comparator.dart @@ -0,0 +1,12 @@ +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; + +class Comparator { + bool isUserDeletionDataEqual(UserDeletionData data1, UserDeletionData data2) { + return data1.isSuccess == data2.isSuccess && + isAccountMetaEqual(data1.accountMeta, data2.accountMeta); + } + + bool isAccountMetaEqual(AccountMeta accountMeta1, AccountMeta accountMeta2) { + return accountMeta1.appId == accountMeta2.appId; + } +} diff --git a/flutter-sample/core/moengage_flutter_android/test/dataprovider/data_provider.dart b/flutter-sample/core/moengage_flutter_android/test/dataprovider/data_provider.dart new file mode 100644 index 00000000..bd0dc025 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/test/dataprovider/data_provider.dart @@ -0,0 +1,4 @@ +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; + +UserDeletionData userDeletionData = + UserDeletionData(accountMeta: AccountMeta('1234'), isSuccess: true); diff --git a/flutter-sample/core/moengage_flutter_android/test/dataprovider/json_data.dart b/flutter-sample/core/moengage_flutter_android/test/dataprovider/json_data.dart new file mode 100644 index 00000000..78206186 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/test/dataprovider/json_data.dart @@ -0,0 +1,12 @@ +const String appId = '1234'; + +const String userDeletionJson = ''' +{ + "accountMeta": { + "appId": "1234" + }, + "data": { + "isUserDeletionSuccess":true + } +} +'''; diff --git a/flutter-sample/core/moengage_flutter_android/test/parser_test.dart b/flutter-sample/core/moengage_flutter_android/test/parser_test.dart new file mode 100644 index 00000000..2bcae71f --- /dev/null +++ b/flutter-sample/core/moengage_flutter_android/test/parser_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:moengage_flutter_android/src/internal/utils/payload_mapper.dart'; + +import 'comparator.dart'; +import 'dataprovider/data_provider.dart'; +import 'dataprovider/json_data.dart'; + +void main() { + test('Test User Deletion Payload', () { + expect( + Comparator().isUserDeletionDataEqual( + PayloadMapper().deSerializeDeleteUserData(userDeletionJson, appId), + userDeletionData), + true); + }); +} diff --git a/flutter-sample/core/moengage_flutter_ios/CHANGELOG.md b/flutter-sample/core/moengage_flutter_ios/CHANGELOG.md new file mode 100644 index 00000000..d075dae6 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/CHANGELOG.md @@ -0,0 +1,12 @@ +# MoEngage Flutter iOS Plugin + +# 01-12-2023 + +## 1.1.0 +- Updated MoEngage-iOS-SDK to 9.14.0 +- Updated MoEngageInApp to 4.13.0 + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_ios/LICENSE b/flutter-sample/core/moengage_flutter_ios/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_ios/README.md b/flutter-sample/core/moengage_flutter_ios/README.md new file mode 100644 index 00000000..3acf6619 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/README.md @@ -0,0 +1,15 @@ +# moengage\_flutter\_ios + +The iOS implementation of [`moengage_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_flutter` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_ios/ios/.gitignore b/flutter-sample/core/moengage_flutter_ios/ios/.gitignore new file mode 100644 index 00000000..aa479fd3 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/.gitignore @@ -0,0 +1,37 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_ios/ios/Assets/.gitkeep b/flutter-sample/core/moengage_flutter_ios/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterBridge.swift b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterBridge.swift new file mode 100644 index 00000000..ad2ca65a --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterBridge.swift @@ -0,0 +1,114 @@ +import Flutter +import UIKit +import MoEngagePluginBase + +public class MoEngageFlutterBridge: NSObject, FlutterPlugin { + + private static var channel : FlutterMethodChannel? = nil + + public static func register(with registrar: FlutterPluginRegistrar) { + channel = FlutterMethodChannel(name: MoEngageFlutterConstants.kPluginChannelName, binaryMessenger: registrar.messenger()) + let instance = MoEngageFlutterBridge() + registrar.addMethodCallDelegate(instance, channel: channel!) + } + + // MARK:- Handle Invocation + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case MoEngageFlutterConstants.MethodNames.kRegisterForPush: + MoEngagePluginBridge.sharedInstance.registerForPush() + + default: + handleWithPayload(call: call) + } + } + + private func handleWithPayload(call: FlutterMethodCall) { + guard let payload = call.arguments as? [String: Any] else { return } + switch call.method { + case MoEngageFlutterConstants.MethodNames.kInitializeFlutter: + pluginInitialized(payload: payload) + + case MoEngageFlutterConstants.MethodNames.kShowInApp: + MoEngagePluginBridge.sharedInstance.showInApp(payload) + case MoEngageFlutterConstants.MethodNames.kGetSelfHandledInApp: + MoEngagePluginBridge.sharedInstance.getSelfHandledInApp(payload) + case MoEngageFlutterConstants.MethodNames.kUpdateSelfHandledInAppState: + MoEngagePluginBridge.sharedInstance.updateSelfHandledImpression(payload) + case MoEngageFlutterConstants.MethodNames.kSetAppContext: + MoEngagePluginBridge.sharedInstance.setInAppContext(payload) + case MoEngageFlutterConstants.MethodNames.kInvalidateAppContext: + MoEngagePluginBridge.sharedInstance.resetInAppContext(payload) + + + case MoEngageFlutterConstants.MethodNames.kSetAppStatus: + MoEngagePluginBridge.sharedInstance.setAppStatus(payload) + case MoEngageFlutterConstants.MethodNames.kOptOutTracking: + MoEngagePluginBridge.sharedInstance.optOutDataTracking(payload) + case MoEngageFlutterConstants.MethodNames.kUpdateSDKState: + MoEngagePluginBridge.sharedInstance.updateSDKState(payload) + case MoEngageFlutterConstants.MethodNames.kTrackEvent: + MoEngagePluginBridge.sharedInstance.trackEvent(payload) + case MoEngageFlutterConstants.MethodNames.kSetUserAttribute: + MoEngagePluginBridge.sharedInstance.setUserAttribute(payload) + case MoEngageFlutterConstants.MethodNames.kSetAlias: + MoEngagePluginBridge.sharedInstance.setAlias(payload) + case MoEngageFlutterConstants.MethodNames.kResetUser: + MoEngagePluginBridge.sharedInstance.resetUser(payload) + + default: + print("Invalid invocation: \(call.method)") + } + } + private func pluginInitialized(payload: [String: Any]){ + MoEngagePluginBridge.sharedInstance.setPluginBridgeDelegate(self, payload: payload) + MoEngagePluginBridge.sharedInstance.pluginInitialized(payload) + } +} + + +extension MoEngageFlutterBridge: MoEngagePluginBridgeDelegate{ + public func sendMessage(event: String, message: [String : Any]) { + if let callbackName = getCallbackName(forEventName: event) { + do { + let jsonData = try JSONSerialization.data(withJSONObject: message) + if let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue){ + MoEngageFlutterBridge.sendCallback(callbackName, withInfo: jsonString) + return + } + MoEngageFlutterBridge.sendCallback(callbackName, withInfo: "{}") + } catch let error { + print(error.localizedDescription) + } + } + } + + // MARK: Utils + func getCallbackName(forEventName name: String) -> String? { + switch name { + case MoEngagePluginConstants.CallBackEvents.pushTokenGenerated: + return MoEngageFlutterConstants.CallbackNames.kPushTokenGenerated + case MoEngagePluginConstants.CallBackEvents.pushClicked: + return MoEngageFlutterConstants.CallbackNames.kPushClicked + case MoEngagePluginConstants.CallBackEvents.inAppShown: + return MoEngageFlutterConstants.CallbackNames.kInAppShown + case MoEngagePluginConstants.CallBackEvents.inAppClicked: + return MoEngageFlutterConstants.CallbackNames.kInAppClicked + case MoEngagePluginConstants.CallBackEvents.inAppCustomAction: + return MoEngageFlutterConstants.CallbackNames.kInAppClickedCustomAction + case MoEngagePluginConstants.CallBackEvents.inAppDismissed: + return MoEngageFlutterConstants.CallbackNames.kInAppDismissed + case MoEngagePluginConstants.CallBackEvents.inAppSelfHandled: + return MoEngageFlutterConstants.CallbackNames.kInAppSelfHandled + default: + return nil + } + } + + // MARK: Send Callback to Flutter + internal static func sendCallback(_ callbackName: String, withInfo info: NSString) { + DispatchQueue.main.async { + channel?.invokeMethod(callbackName, arguments: info) + } + } +} diff --git a/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterConstants.swift b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterConstants.swift new file mode 100644 index 00000000..2b074002 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterConstants.swift @@ -0,0 +1,43 @@ +// +// MoEngageFlutterConstants.swift +// flutter_moengage_plugin +// +// Created by Chengappa C D on 09/12/19. +// + +import Foundation + +struct MoEngageFlutterConstants{ + + static let kPluginChannelName = "com.moengage/core" + static let kPluginName = "flutter" + + struct MethodNames { + static let kInitializeFlutter = "initialise" + static let kSetAppStatus = "setAppStatus" + static let kTrackEvent = "trackEvent" + static let kSetUserAttribute = "setUserAttribute" + static let kSetAlias = "setAlias" + static let kRegisterForPush = "registerForPush" + static let kShowInApp = "showInApp" + static let kGetSelfHandledInApp = "selfHandledInApp" + static let kUpdateSelfHandledInAppState = "selfHandledCallback" + static let kSetAppContext = "setAppContext" + static let kInvalidateAppContext = "resetCurrentContext" + static let kOptOutTracking = "optOutTracking" + static let kUpdateSDKState = "updateSdkState" + static let kStartGeofence = "startGeofenceMonitoring" + static let kEnableLogs = "enableSDKLogs" + static let kResetUser = "logout" + } + + struct CallbackNames { + static let kPushTokenGenerated = "onPushTokenGenerated" + static let kPushClicked = "onPushClick" + static let kInAppShown = "onInAppShown" + static let kInAppClicked = "onInAppClick" + static let kInAppClickedCustomAction = "onInAppCustomAction" + static let kInAppDismissed = "onInAppDismiss" + static let kInAppSelfHandled = "onInAppSelfHandle" + } +} diff --git a/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPlugin.h b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPlugin.h new file mode 100644 index 00000000..3d18d464 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface MoEngageFlutterPlugin : NSObject +@end diff --git a/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPlugin.m b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPlugin.m new file mode 100644 index 00000000..60097b37 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPlugin.m @@ -0,0 +1,8 @@ +#import "MoEngageFlutterPlugin.h" +#import + +@implementation MoEngageFlutterPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [MoEngageFlutterBridge registerWithRegistrar:registrar]; +} +@end diff --git a/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPluginInfo.swift b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPluginInfo.swift new file mode 100644 index 00000000..d4fac429 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageFlutterPluginInfo.swift @@ -0,0 +1,5 @@ +// Generated file, do not edit +import Foundation +struct MoEngageFlutterPluginInfo{ + static let kVersion = "1.1.0" +} diff --git a/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageInitializer.swift b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageInitializer.swift new file mode 100644 index 00000000..c51ea09a --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/Classes/MoEngageInitializer.swift @@ -0,0 +1,48 @@ +// +// MoEngageInitializer.swift +// flutter_moengage_plugin +// +// Created by Chengappa C D on 11/12/19. +// + +import Foundation +import UserNotifications +import MoEngagePluginBase +import MoEngageSDK + +@objc public class MoEngageInitializer : NSObject { + + @objc static public let sharedInstance = MoEngageInitializer() + private override init() {super.init()} + + func getCoreVersion() -> String { + guard + let path = Bundle.main.url( + forResource: "config", withExtension: "json", + subdirectory: "Frameworks/App.framework/flutter_assets/packages/moengage_flutter" + ), + let data = try? Data(contentsOf: path), + let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let version = obj["version"] as? String + else { return "" } + return version + } + + @available(*, deprecated, message: "use 'initializeDefaultInstance(config:sdkState:launchOptions:)'") + @objc public func initializeDefaultInstance(_ config: MoEngageSDKConfig, sdkState: Bool = true, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) { + let currentSDKState: MoEngageSDKState = sdkState ? .enabled: .disabled + initializeDefaultInstance(config: config, sdkState: currentSDKState, launchOptions: launchOptions) + } + + @objc public func initializeDefaultInstance(config: MoEngageSDKConfig, sdkState: MoEngageSDKState = .enabled, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) { + let plugin = MoEngagePlugin() + plugin.initializeDefaultInstance(sdkConfig: config, sdkState: sdkState, launchOptions: launchOptions) + plugin.trackPluginInfo(MoEngageFlutterConstants.kPluginName, version: getCoreVersion()) + } + + @objc public func initializeDefaultInstance(_ config: MoEngageSDKConfig, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) { + let plugin = MoEngagePlugin() + plugin.initializeDefaultInstance(sdkConfig: config, launchOptions: launchOptions) + plugin.trackPluginInfo(MoEngageFlutterConstants.kPluginName, version: getCoreVersion()) + } +} diff --git a/flutter-sample/core/moengage_flutter_ios/ios/moengage_flutter_ios.podspec b/flutter-sample/core/moengage_flutter_ios/ios/moengage_flutter_ios.podspec new file mode 100644 index 00000000..f929970f --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/ios/moengage_flutter_ios.podspec @@ -0,0 +1,31 @@ + +require 'yaml' +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +libraryVersion = pubspec['version'].gsub('+', '-') + +Pod::Spec.new do |s| + s.name = 'moengage_flutter_ios' + s.version = libraryVersion + s.platform = :ios + s.ios.deployment_target = '11.0' + s.summary = 'A flutter plugin for MoEngage iOS and Android SDKs.' + s.description = <<-DESC + A flutter plugin for MoEngage iOS and Android SDKs. + DESC + s.homepage = 'https://www.moengage.com/' + s.license = { :file => '../LICENSE' } + s.author = { 'MoEngage Inc.' => 'mobiledevs@moengage.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.dependency 'MoEngagePluginBase', '~> 4.5.1' + s.swift_version = '5.0' + s.prepare_command = <<-CMD + echo // Generated file, do not edit > Classes/MoEngageFlutterPluginInfo.swift + echo "import Foundation" >> Classes/MoEngageFlutterPluginInfo.swift + echo "struct MoEngageFlutterPluginInfo{\n static let kVersion = \\"#{libraryVersion}\\" \n }" >> Classes/MoEngageFlutterPluginInfo.swift + CMD + +end + diff --git a/flutter-sample/core/moengage_flutter_ios/lib/moengage_flutter_ios.dart b/flutter-sample/core/moengage_flutter_ios/lib/moengage_flutter_ios.dart new file mode 100644 index 00000000..0f7325a7 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/lib/moengage_flutter_ios.dart @@ -0,0 +1,237 @@ +import 'package:flutter/services.dart'; +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; + +/// The iOS implementation of [MoEngageFlutterPlatform]. +class MoEngageFlutterIOS extends MoEngageFlutterPlatform { + /// The method channel used to interact with the native platform. + final MethodChannel _channel = const MethodChannel(channelName); + + /// Registers this class as the default instance of [MoEngageFlutterPlatform] + static void registerWith() { + Logger.v('Registering MoEngageFlutterIOS with Platform Interface'); + MoEngageFlutterPlatform.instance = MoEngageFlutterIOS(); + } + + @override + void initialise(MoEInitConfig moEInitConfig, String appId) { + _channel.invokeMethod(methodInitialise, + InitConfigPayloadMapper().getInitPayload(appId, moEInitConfig)); + } + + @override + void trackEvent( + String eventName, MoEProperties eventAttributes, String appId) { + _channel.invokeMethod( + methodTrackEvent, getEventPayload(eventName, eventAttributes, appId)); + } + + @override + void setUniqueId(String uniqueId, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttrNameUniqueId, userAttrTypeGeneral, uniqueId, appId)); + } + + @override + void setAlias(String newUniqueId, String appId) { + _channel.invokeMethod(methodSetAlias, getAliasPayload(newUniqueId, appId)); + } + + @override + void setUserName(String userName, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttrNameUserName, userAttrTypeGeneral, userName, appId)); + } + + @override + void setFirstName(String firstName, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttrNameFirstName, userAttrTypeGeneral, firstName, appId)); + } + + @override + void setLastName(String lastName, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttrNameLastName, userAttrTypeGeneral, lastName, appId)); + } + + @override + void setEmail(String emailId, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttrNameEmailId, userAttrTypeGeneral, emailId, appId)); + } + + @override + void setPhoneNumber(String phoneNumber, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttrNamePhoneNum, userAttrTypeGeneral, phoneNumber, appId)); + } + + @override + void setGender(MoEGender gender, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload(userAttrNameGender, userAttrTypeGeneral, + genderToString(gender), appId)); + } + + @override + void setLocation(MoEGeoLocation location, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload(userAttrNameLocation, userAttrTypeLocation, + location.toMap(), appId)); + } + + @override + void setBirthDate(String birthDate, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttrNameBirtdate, userAttrTypeTimestamp, birthDate, appId)); + } + + @override + void setUserAttribute( + String userAttributeName, dynamic userAttributeValue, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttributeName, userAttrTypeGeneral, userAttributeValue, appId)); + } + + @override + void setUserAttributeIsoDate( + String userAttributeName, String isoDateString, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttributeName, userAttrTypeTimestamp, isoDateString, appId)); + } + + @override + void setUserAttributeLocation( + String userAttributeName, MoEGeoLocation location, String appId) { + _channel.invokeMethod( + methodSetUserAttribute, + getUserAttributePayload( + userAttributeName, userAttrTypeLocation, location.toMap(), appId)); + } + + @override + void setAppStatus(MoEAppStatus appStatus, String appId) { + _channel.invokeListMethod( + methodSetAppStatus, getAppStatusPayload(appStatus, appId)); + } + + @override + void showInApp(String appId) { + _channel.invokeMethod(methodShowInApp, getAccountMeta(appId)); + } + + @override + void getSelfHandledInApp(String appId) { + _channel.invokeMethod(methodSelfHandledInApp, getAccountMeta(appId)); + } + + @override + void selfHandledCallback(Map payload) { + _channel.invokeMethod(methodSelfHandledCallback, payload); + } + + @override + void setCurrentContext(List contexts, String appId) { + _channel.invokeMethod( + methodSetAppContext, getInAppContextPayload(contexts, appId)); + } + + @override + void resetCurrentContext(String appId) { + _channel.invokeMethod(methodResetAppContext, getAccountMeta(appId)); + } + + @override + void registerForPushNotification() { + _channel.invokeMethod(methodiOSRegisterPush); + } + + @override + void optOutDataTracking(bool optOutDataTracking, String appId) { + _channel.invokeMethod( + methodOptOutTracking, + getOptOutTrackingPayload( + gdprOptOutTypeData, optOutDataTracking, appId)); + } + + @override + void logout(String appId) { + _channel.invokeMethod(methodLogout, getAccountMeta(appId)); + } + + @override + void updateSdkState(bool shouldEnableSdk, String appId) { + _channel.invokeMethod( + methodUpdateSdkState, getUpdateSdkStatePayload(shouldEnableSdk, appId)); + } + + @override + void navigateToSettings() { + Logger.v('navigateToSettings(): Not supported in iOS Platform'); + } + + @override + void onOrientationChanged() { + Logger.v('onOrientationChanged(): Not supported in iOS Platform'); + } + + @override + void passPushPayload( + Map payload, MoEPushService pushService, String appId) { + Logger.v('passPushPayload(): Not supported in iOS Platform'); + } + + @override + void passPushToken( + String pushToken, MoEPushService pushService, String appId) { + Logger.v('passPushToken(): Not supported in iOS Platform'); + } + + @override + void permissionResponse(bool isGranted, PermissionType type) { + Logger.v('permissionResponse(): Not supported in iOS Platform'); + } + + @override + void requestPushPermission() { + Logger.v('requestPushPermission(): Not supported in iOS Platform'); + } + + @override + void setupNotificationChannel() { + Logger.v('setupNotificationChannel(): Not supported in iOS Platform'); + } + + @override + void updateDeviceIdentifierTrackingStatus( + String appId, String identifierType, bool state) { + Logger.v( + 'updateDeviceIdentifierTrackingStatus(): Not supported in iOS Platform'); + } + + @override + void updatePushPermissionRequestCountAndroid(int requestCount, String appId) { + Logger.v( + 'updatePushPermissionRequestCountAndroid(): Not supported in iOS Platform'); + } +} diff --git a/flutter-sample/core/moengage_flutter_ios/pubspec.yaml b/flutter-sample/core/moengage_flutter_ios/pubspec.yaml new file mode 100644 index 00000000..e541d022 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/pubspec.yaml @@ -0,0 +1,27 @@ +name: moengage_flutter_ios +description: iOS implementation of the moengage_flutter plugin +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_flutter + platforms: + ios: + pluginClass: MoEngageFlutterPlugin + dartPluginClass: MoEngageFlutterIOS + +dependencies: + flutter: + sdk: flutter + moengage_flutter_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/core/moengage_flutter_ios/pubspec_overrides.yaml b/flutter-sample/core/moengage_flutter_ios/pubspec_overrides.yaml new file mode 100644 index 00000000..1392d9f2 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_ios/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + moengage_flutter_platform_interface: + path: ../moengage_flutter_platform_interface/ \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_platform_interface/CHANGELOG.md b/flutter-sample/core/moengage_flutter_platform_interface/CHANGELOG.md new file mode 100644 index 00000000..80882ab5 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/CHANGELOG.md @@ -0,0 +1,12 @@ +# MoEngage Flutter Platform Interface + +# 07-12-2023 + +## 1.1.0 +- Google Policy - Delete User details API +- Added support for array in user attributes + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_platform_interface/LICENSE b/flutter-sample/core/moengage_flutter_platform_interface/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_platform_interface/README.md b/flutter-sample/core/moengage_flutter_platform_interface/README.md new file mode 100644 index 00000000..b7016ffc --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/README.md @@ -0,0 +1,26 @@ +# moengage_flutter_platform_interface + +A common platform interface for the [`moengage_flutter`][1] plugin. + +This interface allows platform-specific implementations of the `moengage_flutter` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `moengage_flutter`, extend +[`MoEngageFlutterPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`MoEngageFlutterPlatform` by calling +`MoEngageFlutterPlatform.instance = MyPlatformMoEngageFlutter()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../moengage_flutter +[2]: lib/moengage_flutter_platform_interface.dart \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/moengage_flutter_platform_interface.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/moengage_flutter_platform_interface.dart new file mode 100644 index 00000000..6d0cd2bc --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/moengage_flutter_platform_interface.dart @@ -0,0 +1,38 @@ +export 'src/internal/callback/callback_cache.dart'; +export 'src/internal/callback/callbacks.dart'; +export 'src/internal/constants.dart'; +export 'src/internal/core_instance_provider.dart'; +export 'src/internal/logger.dart'; +export 'src/internal/moe_cache.dart'; +export 'src/internal/moe_core_controller.dart'; +export 'src/log_level.dart'; +export 'src/model/account_meta.dart'; +export 'src/model/app_status.dart'; +export 'src/model/gender.dart'; +export 'src/model/geo_location.dart'; +export 'src/model/inapp/action.dart'; +export 'src/model/inapp/campaign_context.dart'; +export 'src/model/inapp/campaign_data.dart'; +export 'src/model/inapp/click_data.dart'; +export 'src/model/inapp/inapp_action_type.dart'; +export 'src/model/inapp/inapp_custom_action.dart'; +export 'src/model/inapp/inapp_data.dart'; +export 'src/model/inapp/navigation_action.dart'; +export 'src/model/inapp/navigation_type.dart'; +export 'src/model/inapp/self_handled_campaign.dart'; +export 'src/model/inapp/self_handled_data.dart'; +export 'src/model/moe_init_config.dart'; +export 'src/model/permission_result.dart'; +export 'src/model/permission_type.dart'; +export 'src/model/properties.dart'; +export 'src/model/push/moe_push_service.dart'; +export 'src/model/push/push_campaign.dart'; +export 'src/model/push/push_campaign_data.dart'; +export 'src/model/push/push_config.dart'; +export 'src/model/push/push_token_data.dart'; +export 'src/model/user_deletion_data.dart'; +export 'src/moengage_flutter_platform_interface.dart'; +export 'src/utils/data_payload_mapper.dart'; +export 'src/utils/in_app_payload_mapper.dart'; +export 'src/utils/init_config_payload_mapper.dart'; +export 'src/utils/utils.dart'; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/callback/callback_cache.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/callback/callback_cache.dart new file mode 100644 index 00000000..1c13b903 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/callback/callback_cache.dart @@ -0,0 +1,19 @@ +import '../../../moengage_flutter_platform_interface.dart'; + +/// Native to Flutter Callback Cache +class CallbackCache { + /// Push Click Callback + PushClickCallbackHandler? pushClickCallbackHandler; + + /// SelfHandledInApp Callback + SelfHandledInAppCallbackHandler? selfHandledInAppCallbackHandler; + + /// InApp Click Callback + InAppClickCallbackHandler? inAppClickCallbackHandler; + + /// InApp Shown Callback + InAppShownCallbackHandler? inAppShownCallbackHandler; + + /// InApp Dismiss Callback + InAppDismissedCallbackHandler? inAppDismissedCallbackHandler; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/callback/callbacks.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/callback/callbacks.dart new file mode 100644 index 00000000..31ed844c --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/callback/callbacks.dart @@ -0,0 +1,29 @@ +import '../../model/inapp/click_data.dart'; +import '../../model/inapp/inapp_data.dart'; +import '../../model/inapp/self_handled_data.dart'; +import '../../model/permission_result.dart'; +import '../../model/push/push_campaign_data.dart'; +import '../../model/push/push_token_data.dart'; + +/// Push Click Callback +typedef PushClickCallbackHandler = void Function(PushCampaignData data); + +/// PushToken Callback +typedef PushTokenCallbackHandler = void Function(PushTokenData data); + +/// Self Handled InApp Available Callback +typedef SelfHandledInAppCallbackHandler = void Function( + SelfHandledCampaignData data); + +/// InApp Click Action Callback +typedef InAppClickCallbackHandler = void Function(ClickData data); + +/// InApp Shown Callback +typedef InAppShownCallbackHandler = void Function(InAppData data); + +/// InApp Dismiss Callback +typedef InAppDismissedCallbackHandler = void Function(InAppData data); + +/// Push Permission Result Callback +typedef PermissionResultCallbackHandler = void Function( + PermissionResultData data); diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/constants.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/constants.dart new file mode 100644 index 00000000..50e99e1e --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/constants.dart @@ -0,0 +1,156 @@ +// ignore_for_file: public_member_api_docs +// Plugin Channel +const String TAG = 'Core_'; + +// Plugin Channel +const String channelName = 'com.moengage/core'; + +// Method Names +const String methodInitialise = 'initialise'; +const String methodSetAppStatus = 'setAppStatus'; +const String methodTrackEvent = 'trackEvent'; +const String methodSetUserAttribute = 'setUserAttribute'; +const String methodSetAlias = 'setAlias'; +const String methodiOSRegisterPush = 'registerForPush'; +const String methodPushToken = 'pushToken'; +const String methodPushPayLoad = 'pushPayload'; +const String methodShowInApp = 'showInApp'; +const String methodSelfHandledInApp = 'selfHandledInApp'; +const String methodSelfHandledCallback = 'selfHandledCallback'; +const String methodSetAppContext = 'setAppContext'; +const String methodResetAppContext = 'resetCurrentContext'; +const String methodOptOutTracking = 'optOutTracking'; +const String methodLogout = 'logout'; +const String methodUpdateSdkState = 'updateSdkState'; +const String methodOnOrientationChanged = 'onOrientationChanged'; +const String methodDeviceIdentifierTracking = 'deviceIdentifierTracking'; +const String methodUpdateDeviceIdentifierTrackingStatus = + 'updateDeviceIdentifierTrackingStatus'; +const String methodSetupNotificationChannelAndroid = + 'setupNotificationChannels'; +const String methodNavigateToSettingsAndroid = 'navigateToSettings'; +const String methodRequestPushPermissionAndroid = 'requestPushPermission'; +const String methodUpdatePushPermissionRequestCount = + 'updatePushPermissionRequestCount'; +const String methodPermissionResponse = 'permissionResponse'; + +// Callback Names +const String callbackOnPushClick = 'onPushClick'; +const String callbackOnInAppShown = 'onInAppShown'; +const String callbackOnInAppClicked = 'onInAppClick'; +const String callbackOnInAppDismissed = 'onInAppDismiss'; +const String callbackOnInAppCustomAction = 'onInAppCustomAction'; +const String callbackOnInAppSelfHandled = 'onInAppSelfHandle'; +const String callbackPushTokenGenerated = 'onPushTokenGenerated'; +const String callbackPermissionResult = 'onPermissionResult'; + +// Gender Value Constants +const String genderMale = 'male'; +const String genderFemale = 'female'; +const String genderOther = 'other'; + +// AppStatus Value Constants +const String appStatusInstall = 'INSTALL'; +const String appStatusUpdate = 'UPDATE'; + +// Default User Attribute Names +const String userAttrNameUniqueId = 'USER_ATTRIBUTE_UNIQUE_ID'; +const String userAttrNameUserName = 'USER_ATTRIBUTE_USER_NAME'; +const String userAttrNameFirstName = 'USER_ATTRIBUTE_USER_FIRST_NAME'; +const String userAttrNameLastName = 'USER_ATTRIBUTE_USER_LAST_NAME'; +const String userAttrNameEmailId = 'USER_ATTRIBUTE_USER_EMAIL'; +const String userAttrNamePhoneNum = 'USER_ATTRIBUTE_USER_MOBILE'; +const String userAttrNameGender = 'USER_ATTRIBUTE_USER_GENDER'; +const String userAttrNameBirtdate = 'USER_ATTRIBUTE_USER_BDAY'; +const String userAttrNameLocation = 'USER_ATTRIBUTE_USER_LOCATION'; + +// Keys Constants +const String keyEventName = 'eventName'; +const String keyEventAttributes = 'eventAttributes'; +const String keyAttributeValue = 'attributeValue'; +const String keyAttributeName = 'attributeName'; +const String keyAttrLatitudeName = 'latitude'; +const String keyAttrLongitudeName = 'longitude'; +const String keyPushToken = 'token'; +const String keyPushPayload = 'pushPayload'; +const String keyService = 'service'; +const String keyAttributeType = 'type'; +const String keyAlias = 'alias'; +const String keyLocationAttribute = 'locationAttribute'; +const String keyState = 'state'; +const String keyAppStatus = 'appStatus'; +const String keyContexts = 'contexts'; +const String keyPushService = 'pushService'; + +const String keyPayload = 'payload'; +const String keyKvPair = 'kvPair'; + +//InApp Campaign Constants +const String keyPlatform = 'platform'; +const String keyCampaignId = 'campaignId'; +const String keyCampaignName = 'campaignName'; +const String keyNavigation = 'navigation'; +const String keySelfHandled = 'selfHandled'; +const String keyCustomAction = 'customAction'; +const String keyCampaignContext = 'campaignContext'; +const String keyFormattedCampaignId = 'cid'; +const String keyActionType = 'actionType'; +const String keyType = 'type'; + +// Navigation action Constants +const String keyNavigationType = 'navigationType'; +const String keyValue = 'value'; + +// SelHandled InApp Constants +const String keyDismissInterval = 'dismissInterval'; + +//PushPayload Constants +const String keyIsDefaultAction = 'isDefaultAction'; +const String keyClickedAction = 'clickedAction'; + +// User Attribute Type value Constants +const String userAttrTypeGeneral = 'general'; +const String userAttrTypeTimestamp = 'timestamp'; +const String userAttrTypeLocation = 'location'; + +// GDPR Opt-Outs Constants +const String gdprOptOutTypeData = 'data'; + +// SelfHandled Callback Action Types +const String selfHandledActionShown = 'impression'; +const String selfHandledActionClick = 'click'; +const String selfHandledActionDismissed = 'dismissed'; + +// SDK Status update +const String keyIsSdkEnabled = 'isSdkEnabled'; + +const String keyAppId = 'appId'; +const String keyAccountMeta = 'accountMeta'; +const String keyData = 'data'; +const String keyInitConfig = 'initConfig'; +const String keyPushConfig = 'pushConfig'; + +const String keyAndroidId = 'isAndroidIdTrackingEnabled'; +const String keyAdId = 'isAdIdTrackingEnabled'; +const String keyDeviceId = 'isDeviceIdTrackingEnabled'; + +// permission +const String keyIsPermissionGranted = 'isGranted'; +const String keyPermissionType = 'type'; + +const String keyUpdatePushPermissionCount = 'pushOptinInAttemptCount'; + +//Push Config Keys + +/// Key for Registering for sdk to send only callback on Push Click on App Foreground. +/// MoEngage SDK will not handle the redirection in this case +const String keyShouldDeliverCallbackOnForegroundClick = + 'shouldDeliverCallbackOnForegroundClick'; + +/// Key for Self handled push redirection. If self handled push direction is true, +/// Client is responsible for push redirection on Push Click +const String keySelfHandledPushRedirection = 'selfHandledPushRedirection'; + +/// User Deletion +const String methodNameDeleteUser = 'deleteUser'; +const String keyUserDeletionStatus = 'isUserDeletionSuccess'; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/core_instance_provider.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/core_instance_provider.dart new file mode 100644 index 00000000..2bb4a919 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/core_instance_provider.dart @@ -0,0 +1,25 @@ +import 'callback/callback_cache.dart'; + +/// Instance Specific Cache for Callbacks +class CoreInstanceProvider { + /// [CoreInstanceProvider] Constructor + factory CoreInstanceProvider() => _instance; + + CoreInstanceProvider._internal(); + static final CoreInstanceProvider _instance = + CoreInstanceProvider._internal(); + + final Map _caches = {}; + + /// Get [CallbackCache] instance For provided MoEngage App Id + CallbackCache getCallbackCacheForInstance(String appId) { + final CallbackCache? cache = _caches[appId]; + if (cache != null) { + return cache; + } else { + final CallbackCache instanceCache = CallbackCache(); + _caches[appId] = instanceCache; + return instanceCache; + } + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/logger.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/logger.dart new file mode 100644 index 00000000..110fb939 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/logger.dart @@ -0,0 +1,94 @@ +import 'package:flutter/foundation.dart'; + +import '../log_level.dart'; + +/// Base Tag for Logger +const String BASE_TAG = 'MoEFlutter'; + +///Logger Util Class to print logs to the Flutter Console +class Logger { + /// Factory Constructor + factory Logger() => _logger; + + Logger._(); + static final Logger _logger = Logger._(); + + ///Flag to handle logs in Release build. By default logs in release mode will be disabled. + bool _isEnabledForReleaseBuild = false; + + ///LogLevel for SDK logs. + LogLevel _logLevel = LogLevel.INFO; + + /// Configure MoEngage SDK Logs + /// [logLevel] - [LogLevel] Control for SDK logs + /// [isEnabledForReleaseBuild] If true, logs will be printed for the Release build. By default the logs are disabled for the Release build. + static void configureLogs(LogLevel logLevel, + [bool isEnabledForReleaseBuild = false]) { + _logger._logLevel = logLevel; + _logger._isEnabledForReleaseBuild = isEnabledForReleaseBuild; + } + + ///Logs INFO level messages + ///[message] message to be logged + static void i(String message) { + _log('[I]: $BASE_TAG $message', logLevel: LogLevel.INFO); + } + + ///Logs DEBUG level messages + ///[message] message to be logged + static void d(String message) { + _log('[D]: $BASE_TAG $message', logLevel: LogLevel.DEBUG); + } + + ///Logs WARN level messages. + ///[message] message to be logged + static void w(String message) { + _log('[W]: $BASE_TAG $message', + logLevel: LogLevel.WARN, textColor: '\x1B[33m'); + } + + ///Logs ERROR level messages. + ///[message] message to be logged + ///[error] optional named argument of type [Error]/[Exception]. + ///[stackTrace] optional named argument [StackTrace] + static void e(String message, {dynamic error, StackTrace? stackTrace}) { + _log('[E]: $BASE_TAG $message', + error: error, + logLevel: LogLevel.ERROR, + stackTrace: stackTrace, + textColor: '\x1B[31m'); + } + + ///Logs VERBOSE level messages + ///[message] message to be logged + static void v(String message) { + _log('[V]: $BASE_TAG $message', logLevel: LogLevel.VERBOSE); + } + + static void _log(String message, + {dynamic error, + StackTrace? stackTrace, + required LogLevel logLevel, + String? textColor}) { + if (_logger._logLevel.index < logLevel.index) { + return; + } + if (kDebugMode || kProfileMode || _logger._isEnabledForReleaseBuild) { + debugPrint( + _buildMessage(message, logLevel, stackTrace, error, textColor)); + } + } + + static String _buildMessage(String message, LogLevel logLevel, + StackTrace? stackTrace, dynamic error, String? textColor) { + final StringBuffer resultMessage = StringBuffer(); + resultMessage + ..write(textColor ?? '') + ..write('${DateTime.now()} ') + ..write(message) + ..write(error ?? '') + ..write(stackTrace ?? '') + ..write((textColor != null) ? '\x1B[0m' : ''); + return resultMessage.toString(); + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/method_channel_moengage_flutter.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/method_channel_moengage_flutter.dart new file mode 100644 index 00000000..8fc3f812 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/method_channel_moengage_flutter.dart @@ -0,0 +1,282 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +import '../../moengage_flutter_platform_interface.dart'; + +/// An implementation of [MoEngageFlutterPlatform] that uses method channels. +class MethodChannelMoEngageFlutter extends MoEngageFlutterPlatform { + /// The method channel used to interact with the native platform. + final MethodChannel _methodChannel = const MethodChannel(channelName); + + @override + void initialise(MoEInitConfig moEInitConfig, String appId) { + _methodChannel.invokeMethod(methodInitialise, + InitConfigPayloadMapper().getInitPayload(appId, moEInitConfig)); + } + + @override + void trackEvent( + String eventName, MoEProperties eventAttributes, String appId) { + _methodChannel.invokeMethod(methodTrackEvent, + json.encode(getEventPayload(eventName, eventAttributes, appId))); + } + + @override + void setUniqueId(String uniqueId, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameUniqueId, userAttrTypeGeneral, uniqueId, appId)); + } + + @override + void setAlias(String newUniqueId, String appId) { + _methodChannel.invokeMethod( + methodSetAlias, json.encode(getAliasPayload(newUniqueId, appId))); + } + + @override + void setUserName(String userName, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameUserName, userAttrTypeGeneral, userName, appId)); + } + + @override + void setFirstName(String firstName, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameFirstName, userAttrTypeGeneral, firstName, appId)); + } + + @override + void setLastName(String lastName, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameLastName, userAttrTypeGeneral, lastName, appId)); + } + + @override + void setEmail(String emailId, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameEmailId, userAttrTypeGeneral, emailId, appId)); + } + + @override + void setPhoneNumber(String phoneNumber, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNamePhoneNum, userAttrTypeGeneral, phoneNumber, appId)); + } + + @override + void setGender(MoEGender gender, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson(userAttrNameGender, userAttrTypeGeneral, + genderToString(gender), appId)); + } + + @override + void setLocation(MoEGeoLocation location, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson(userAttrNameLocation, userAttrTypeLocation, + location.toMap(), appId)); + } + + @override + void setBirthDate(String birthDate, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttrNameBirtdate, userAttrTypeTimestamp, birthDate, appId)); + } + + @override + void setUserAttribute( + String userAttributeName, dynamic userAttributeValue, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttributeName, userAttrTypeGeneral, userAttributeValue, appId)); + } + + @override + void setUserAttributeIsoDate( + String userAttributeName, String isoDateString, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttributeName, userAttrTypeTimestamp, isoDateString, appId)); + } + + @override + void setUserAttributeLocation( + String userAttributeName, MoEGeoLocation location, String appId) { + _methodChannel.invokeMethod( + methodSetUserAttribute, + _getUserAttributePayloadJson( + userAttributeName, userAttrTypeLocation, location.toMap(), appId)); + } + + @override + void setAppStatus(MoEAppStatus appStatus, String appId) { + _methodChannel.invokeMethod( + methodSetAppStatus, json.encode(getAppStatusPayload(appStatus, appId))); + } + + @override + void showInApp(String appId) { + _methodChannel.invokeMethod( + methodShowInApp, json.encode(getAccountMeta(appId))); + } + + @override + void logout(String appId) { + _methodChannel.invokeMethod( + methodLogout, json.encode(getAccountMeta(appId))); + } + + @override + void getSelfHandledInApp(String appId) { + _methodChannel.invokeMethod( + methodSelfHandledInApp, json.encode(getAccountMeta(appId))); + } + + @override + void selfHandledCallback(Map payload) { + _methodChannel.invokeMethod( + methodSelfHandledCallback, json.encode(payload)); + } + + @override + void setCurrentContext(List contexts, String appId) { + _methodChannel.invokeMethod(methodSetAppContext, + json.encode(getInAppContextPayload(contexts, appId))); + } + + @override + void resetCurrentContext(String appId) { + _methodChannel.invokeMethod( + methodResetAppContext, json.encode(getAccountMeta(appId))); + } + + @override + void passPushToken( + String pushToken, MoEPushService pushService, String appId) { + _methodChannel.invokeMethod( + methodPushToken, _getPushTokenPayload(pushToken, pushService, appId)); + } + + @override + void optOutDataTracking(bool optOutDataTracking, String appId) { + _methodChannel.invokeMethod( + methodOptOutTracking, + json.encode(getOptOutTrackingPayload( + gdprOptOutTypeData, optOutDataTracking, appId))); + } + + @override + void registerForPushNotification() { + Logger.v('registerForPushNotification() method not available for Android'); + } + + @override + void passPushPayload( + Map payload, MoEPushService pushService, String appId) { + final String pushPayload = _getPushPayload(payload, pushService, appId); + _methodChannel.invokeMethod(methodPushPayLoad, pushPayload); + } + + @override + void updateSdkState(bool shouldEnableSdk, String appId) { + _methodChannel.invokeMethod(methodUpdateSdkState, + json.encode(getUpdateSdkStatePayload(shouldEnableSdk, appId))); + } + + @override + void onOrientationChanged() { + _methodChannel.invokeMethod(methodOnOrientationChanged); + } + + @override + void updateDeviceIdentifierTrackingStatus( + String appId, String identifierType, bool state) { + _methodChannel.invokeListMethod(methodUpdateDeviceIdentifierTrackingStatus, + _getDeviceIdentifierJson(appId, identifierType, state)); + } + + @override + void setupNotificationChannel() { + _methodChannel.invokeMethod(methodSetupNotificationChannelAndroid); + } + + @override + void permissionResponse(bool isGranted, PermissionType type) { + _methodChannel.invokeMethod(methodPermissionResponse, + json.encode(getPermissionResponsePayload(isGranted, type))); + } + + @override + void navigateToSettings() { + _methodChannel.invokeMethod(methodNavigateToSettingsAndroid); + } + + @override + void requestPushPermission() { + _methodChannel.invokeMethod(methodRequestPushPermissionAndroid); + } + + @override + void updatePushPermissionRequestCountAndroid(int requestCount, String appId) { + _methodChannel.invokeMethod(methodUpdatePushPermissionRequestCount, + _getUpdatePushCountJsonPayload(requestCount, appId)); + } + + String _getUserAttributePayloadJson(String attributeName, + String attributeType, dynamic attributeValue, String appId) { + return json.encode(getUserAttributePayload( + attributeName, attributeType, attributeValue, appId)); + } + + String _getPushTokenPayload( + String pushToken, MoEPushService pushService, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = { + keyPushToken: pushToken, + keyService: pushService.asString + }; + return json.encode(payload); + } + + String _getPushPayload(Map pushPayload, + MoEPushService pushService, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = { + keyPayload: pushPayload, + keyService: pushService.asString + }; + return json.encode(payload); + } + + String _getDeviceIdentifierJson( + String appId, String identifierType, bool state) { + final Map payload = getAccountMeta(appId); + payload[keyData] = {identifierType: state}; + return json.encode(payload); + } + + String _getUpdatePushCountJsonPayload(int requestCount, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = {keyUpdatePushPermissionCount: requestCount}; + return jsonEncode(payload); + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/moe_cache.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/moe_cache.dart new file mode 100644 index 00000000..1a117e81 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/moe_cache.dart @@ -0,0 +1,17 @@ +import 'callback/callbacks.dart'; + +/// Core Cache +class Cache { + /// Factory Constructor + factory Cache() => _instance; + + Cache._internal(); + + static final Cache _instance = Cache._internal(); + + /// Push Token Result Callback + PushTokenCallbackHandler? pushTokenCallbackHandler; + + /// Permission Result Callback + PermissionResultCallbackHandler? permissionResultCallbackHandler; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/moe_core_controller.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/moe_core_controller.dart new file mode 100644 index 00000000..51b576b9 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/internal/moe_core_controller.dart @@ -0,0 +1,124 @@ +import 'package:flutter/services.dart'; + +import '../model/inapp/click_data.dart'; +import '../model/inapp/inapp_data.dart'; +import '../model/inapp/self_handled_data.dart'; +import '../model/permission_result.dart'; +import '../model/push/push_campaign_data.dart'; +import '../model/push/push_token_data.dart'; +import '../utils/in_app_payload_mapper.dart'; +import '../utils/push_payload_mapper.dart'; +import '../utils/utils.dart'; +import 'callback/callbacks.dart'; +import 'constants.dart'; +import 'core_instance_provider.dart'; +import 'logger.dart'; +import 'moe_cache.dart'; + +/// Native to Flutter Method Channel Controller +class CoreController { + /// Factory Constructor + factory CoreController.init() => _instance; + + CoreController._internal() { + _channel.setMethodCallHandler(_handler); + } + + final String _tag = '${TAG}CoreController'; + static final CoreController _instance = CoreController._internal(); + final MethodChannel _channel = const MethodChannel(channelName); + + Future _handler(MethodCall call) async { + Logger.v( + '$_tag _handler() : Received callback. Payload ${call.method} - ${call.arguments}'); + try { + if (call.method == callbackPushTokenGenerated) { + final PushTokenData? data = + PushPayloadMapper().pushTokenFromJson(call.arguments); + if (data != null) { + final PushTokenCallbackHandler? handler = + Cache().pushTokenCallbackHandler; + if (handler != null) { + handler.call(data); + } + } + } + if (call.method == callbackOnPushClick) { + final PushCampaignData? data = + PushPayloadMapper().pushCampaignFromJson(call.arguments); + if (data != null) { + final PushClickCallbackHandler? handler = CoreInstanceProvider() + .getCallbackCacheForInstance(data.accountMeta.appId) + .pushClickCallbackHandler; + if (handler != null) { + handler.call(data); + } + } + } + if (call.method == callbackOnInAppClicked || + call.method == callbackOnInAppCustomAction) { + final ClickData? data = + InAppPayloadMapper().actionFromJson(call.arguments); + if (data != null) { + final InAppClickCallbackHandler? handler = CoreInstanceProvider() + .getCallbackCacheForInstance(data.accountMeta.appId) + .inAppClickCallbackHandler; + if (handler != null) { + handler.call(data); + } + } + } + if (call.method == callbackOnInAppShown) { + final InAppData? data = + InAppPayloadMapper().inAppDataFromJson(call.arguments); + if (data != null) { + final InAppShownCallbackHandler? handler = CoreInstanceProvider() + .getCallbackCacheForInstance(data.accountMeta.appId) + .inAppShownCallbackHandler; + if (handler != null) { + handler.call(data); + } + } + } + if (call.method == callbackOnInAppDismissed) { + final InAppData? data = + InAppPayloadMapper().inAppDataFromJson(call.arguments); + if (data != null) { + final InAppDismissedCallbackHandler? handler = CoreInstanceProvider() + .getCallbackCacheForInstance(data.accountMeta.appId) + .inAppDismissedCallbackHandler; + if (handler != null) { + handler.call(data); + } + } + } + if (call.method == callbackOnInAppSelfHandled) { + final SelfHandledCampaignData? data = + InAppPayloadMapper().selfHandledCampaignFromJson(call.arguments); + Logger.i('$_tag _handler() : data: $data'); + if (data != null) { + final SelfHandledInAppCallbackHandler? handler = + CoreInstanceProvider() + .getCallbackCacheForInstance(data.accountMeta.appId) + .selfHandledInAppCallbackHandler; + Logger.v('$_tag _handler() : handler: $handler'); + if (handler != null) { + handler.call(data); + } + } + } + if (call.method == callbackPermissionResult) { + final PermissionResultCallbackHandler? handler = + Cache().permissionResultCallbackHandler; + if (handler != null) { + final PermissionResultData data = + permissionResultFromMap(call.arguments); + handler.call(data); + } + } + } catch (e, stackTrace) { + Logger.e('$_tag Error: $call has an Exception:', + error: e, stackTrace: stackTrace); + } + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/log_level.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/log_level.dart new file mode 100644 index 00000000..6a759cbe --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/log_level.dart @@ -0,0 +1,20 @@ +///Log Level to handle type of Log +enum LogLevel { + /// No logs from the SDK would be printed. + NO_LOG, + + /// Error logs from the SDK would be printed. + ERROR, + + /// Warning logs from the SDK would be printed. + WARN, + + /// Info logs from the SDK would be printed. + INFO, + + /// Debug logs from the SDK would be printed. + DEBUG, + + /// Verbose logs from the SDK would be printed. + VERBOSE +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/account_meta.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/account_meta.dart new file mode 100644 index 00000000..4c46ec39 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/account_meta.dart @@ -0,0 +1,13 @@ +/// Account Meta +class AccountMeta { + /// [AccountMeta] Constructor + AccountMeta(this.appId); + + /// MoEngage AppId + String appId; + + @override + String toString() { + return '{\nappId: $appId\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/app_status.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/app_status.dart new file mode 100644 index 00000000..927c97dc --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/app_status.dart @@ -0,0 +1,8 @@ +/// Application Status +enum MoEAppStatus { + /// Fresh installation of the app instance. + install, + + /// App was already present and user has updated the app + update +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/gender.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/gender.dart new file mode 100644 index 00000000..0a44361c --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/gender.dart @@ -0,0 +1,25 @@ +import '../internal/constants.dart'; + +/// User attribute Gender options +enum MoEGender { + /// User Gender Male + male, + + /// User Gender Female + female, + + /// User Gender Other + other +} + +/// Convert Gender to String +String genderToString(MoEGender gender) { + switch (gender) { + case MoEGender.male: + return genderMale; + case MoEGender.female: + return genderFemale; + case MoEGender.other: + return genderOther; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/geo_location.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/geo_location.dart new file mode 100644 index 00000000..8f6393bd --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/geo_location.dart @@ -0,0 +1,21 @@ +/// User Attribute Location Attribute +class MoEGeoLocation { + /// [MoEGeoLocation] Constructor + MoEGeoLocation(this.latitude, this.longitude); + + ///Latitude of location + double latitude; + + /// Longitude of location + double longitude; + + /// Convert [MoEGeoLocation] to Json [Map] + Map toMap() { + return {'latitude': latitude, 'longitude': longitude}; + } + + @override + String toString() { + return '{\nlatitude: $latitude\nlatitude: $longitude\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/action.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/action.dart new file mode 100644 index 00000000..23a2c238 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/action.dart @@ -0,0 +1,10 @@ +import '../inapp/inapp_action_type.dart'; + +/// InApp Base Action +abstract class Action { + /// Base Constructor for Action + Action(this.actionType); + + /// Type of Action + ActionType actionType; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/campaign_context.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/campaign_context.dart new file mode 100644 index 00000000..ac268904 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/campaign_context.dart @@ -0,0 +1,16 @@ +/// Context of InApp Campaign +class CampaignContext { + /// [CampaignContext] Constructor + CampaignContext(this.formattedCampaignId, this.attributes); + + /// Formatted Campaign Id + String formattedCampaignId; + + /// Campaign Attributes + Map attributes; + + @override + String toString() { + return '{\nformattedCampaignId: $formattedCampaignId\nattributes: $attributes\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/campaign_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/campaign_data.dart new file mode 100644 index 00000000..c30df692 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/campaign_data.dart @@ -0,0 +1,21 @@ +import '../inapp/campaign_context.dart'; + +/// InApp Campaign Data +class CampaignData { + /// [CampaignData] Constructor + CampaignData(this.campaignId, this.campaignName, this.context); + + /// InApp Campaign Id + String campaignId; + + /// InApp Campaign Name + String campaignName; + + /// Campaign Context of type [CampaignContext] + CampaignContext context; + + @override + String toString() { + return '{\ncampaignId: $campaignId\ncampaignName: $campaignName\ncontext: $context\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/click_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/click_data.dart new file mode 100644 index 00000000..76c1101d --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/click_data.dart @@ -0,0 +1,27 @@ +import '../../model/account_meta.dart'; +import '../../model/platforms.dart'; +import '../inapp/action.dart'; +import '../inapp/campaign_data.dart'; + +/// InApp Click Data +class ClickData { + /// [ClickData] Constructor + ClickData(this.platform, this.accountMeta, this.campaignData, this.action); + + /// Type of Platform [Android/IOS] + Platforms platform; + + /// Instance of [AccountMeta] + AccountMeta accountMeta; + + /// InApp Campaign Related Data + CampaignData campaignData; + + /// InApp Action + Action action; + + @override + String toString() { + return '{\nplatform: ${platform.asString}\naccountMeta: $accountMeta\ncampaignData: $campaignData\naction: $action\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_action_type.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_action_type.dart new file mode 100644 index 00000000..afbaf1f1 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_action_type.dart @@ -0,0 +1,8 @@ +/// InApp Action Type +enum ActionType { + /// Navigation Action + navigation, + + /// Custom Action + custom +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_custom_action.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_custom_action.dart new file mode 100644 index 00000000..f2157338 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_custom_action.dart @@ -0,0 +1,17 @@ +import '../inapp/action.dart'; + +/// Custom Action +class CustomAction extends Action { + /// [CustomAction] Constructor + /// [actionType] Instance of [ActionType] + /// [keyValuePairs] Key Value Pair Data + CustomAction(super.actionType, this.keyValuePairs); + + ///Key-Value Pair entered on the MoEngage Platform during campaign creation. + Map keyValuePairs; + + @override + String toString() { + return '{\nactionType: $actionType\nkeyValuePairs: $keyValuePairs\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_data.dart new file mode 100644 index 00000000..b4bead58 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/inapp_data.dart @@ -0,0 +1,23 @@ +import '../../model/account_meta.dart'; +import '../../model/inapp/campaign_data.dart'; +import '../../model/platforms.dart'; + +/// InApp Data +class InAppData { + /// [InAppData] Constructor + InAppData(this.platform, this.accountMeta, this.campaignData); + + /// Type of Platform [Android/IOS] + Platforms platform; + + /// Instance of [AccountMeta] + AccountMeta accountMeta; + + /// InApp Campaign Related Data + CampaignData campaignData; + + @override + String toString() { + return '{\nplatform: ${platform.asString}\naccountMeta: $accountMeta\ncampaignData: $campaignData\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/navigation_action.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/navigation_action.dart new file mode 100644 index 00000000..75884ffa --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/navigation_action.dart @@ -0,0 +1,26 @@ +import '../inapp/action.dart'; +import '../inapp/navigation_type.dart'; + +/// Navigation Action Data +class NavigationAction extends Action { + /// [NavigationAction] Constructor + NavigationAction(super.actionType, this.navigationType, this.navigationUrl, + this.keyValuePairs); + + /// Type of Navigation action. + /// + /// Possible value deep_linking or screen + NavigationType navigationType; + + /// Deeplink Url or the Screen Name used for the action. + String navigationUrl; + + /// [Map] of Key-Value pairs entered on the MoEngage Platform for + /// navigation action of the campaign. + Map keyValuePairs; + + @override + String toString() { + return '{\nactionType: $actionType\nnavigationType: $navigationType\nnavigationUrl: $navigationUrl\nkeyValuePairs: $keyValuePairs\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/navigation_type.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/navigation_type.dart new file mode 100644 index 00000000..ec34af25 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/navigation_type.dart @@ -0,0 +1,27 @@ +/// Navigation Types for Navigation Action +enum NavigationType { + /// Screen Name Navigation + screenName, + + /// DeepLink Navigation + deeplink +} + +/// Extension for Converting Navigation Type Enum to String +extension NavigationTypeExtension on NavigationType { + /// Get [NavigationType] from [String] + static NavigationType fromString(String navigationType) { + switch (navigationType) { + case _screenName: + return NavigationType.screenName; + case _deeplink: + return NavigationType.deeplink; + default: + throw Exception( + 'NavigationType.fromString() $navigationType not a valid navigation type.'); + } + } +} + +const String _screenName = 'screen'; +const String _deeplink = 'deep_linking'; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/self_handled_campaign.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/self_handled_campaign.dart new file mode 100644 index 00000000..1ba6c6a7 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/self_handled_campaign.dart @@ -0,0 +1,16 @@ +/// Self Handled Inpp Campaign Data +class SelfHandledCampaign { + /// [SelfHandledCampaign] Constructor + SelfHandledCampaign(this.payload, this.dismissInterval); + + /// Self handled campaign payload. + String payload; + + /// Interval after which in-app should be dismissed, unit - Seconds + int dismissInterval; + + @override + String toString() { + return '{\npayload: $payload\ndismissInterval: $dismissInterval\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/self_handled_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/self_handled_data.dart new file mode 100644 index 00000000..d5cc69cb --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/inapp/self_handled_data.dart @@ -0,0 +1,28 @@ +import '../../model/account_meta.dart'; +import '../../model/platforms.dart'; +import '../inapp/campaign_data.dart'; +import '../inapp/self_handled_campaign.dart'; + +/// Self Handled InApp Campaign Data +class SelfHandledCampaignData { + /// [SelfHandledCampaignData] Constructor + SelfHandledCampaignData( + this.campaignData, this.accountMeta, this.campaign, this.platform); + + /// Campaign Data of type [CampaignData] + CampaignData campaignData; + + /// Instance of [AccountMeta] + AccountMeta accountMeta; + + /// Self Handled Campaign Data + SelfHandledCampaign campaign; + + /// Type of Platform [Android/IOS] + Platforms platform; + + @override + String toString() { + return '{\ncampaignData: $campaignData\naccountMeta: $accountMeta\ncampaign: $campaign\nplatform: ${platform.asString}\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/moe_init_config.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/moe_init_config.dart new file mode 100644 index 00000000..6521bef0 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/moe_init_config.dart @@ -0,0 +1,14 @@ +import '../model/push/push_config.dart'; + +/// Init Config instance to be passed during initialization. If it is not passed +/// default values will be used. +class MoEInitConfig { + /// [MoEInitConfig] Constructor + MoEInitConfig({required this.pushConfig}); + + /// Named Constructor with Default Config + MoEInitConfig.defaultConfig() : this(pushConfig: PushConfig.defaultConfig()); + + /// Instance of [PushConfig] - Configuration for Handling Push Notification + PushConfig pushConfig; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/permission_result.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/permission_result.dart new file mode 100644 index 00000000..4163755f --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/permission_result.dart @@ -0,0 +1,22 @@ +import '../model/permission_type.dart'; +import '../model/platforms.dart'; + +/// Permission Result Data +class PermissionResultData { + /// [PermissionResultData] Constructor + PermissionResultData(this.platform, this.isGranted, this.type); + + /// Type of Platform [Android/IOS] + Platforms platform; + + /// Permission Granted Status + bool isGranted; + + /// Permission Type + PermissionType type; + + @override + String toString() { + return '{\nplatform: $platform,\nisGranted: $isGranted,\ntype: $type\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/permission_type.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/permission_type.dart new file mode 100644 index 00000000..781e42b3 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/permission_type.dart @@ -0,0 +1,29 @@ +/// Permission Type +enum PermissionType { + /// Push Permission Type + PUSH +} + +/// Permission Type Extension +extension PermissionTypeExtension on PermissionType { + /// [PermissionType] From String + static PermissionType fromString(String permissionType) { + switch (permissionType) { + case _permissionTypePush: + return PermissionType.PUSH; + default: + throw Exception( + 'PermissionType.fromString() $permissionType not a valid platform type.'); + } + } + + /// Convert [PermissionType] to String + String get asString { + switch (this) { + case PermissionType.PUSH: + return _permissionTypePush; + } + } +} + +const String _permissionTypePush = 'push'; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/platforms.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/platforms.dart new file mode 100644 index 00000000..f989c58f --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/platforms.dart @@ -0,0 +1,49 @@ +import 'dart:io'; + +/// Type of Platform +enum Platforms { + /// Android Platform + android, + + /// Platform IOS + iOS +} + +/// Extension for [Platforms] +extension PlatformsExtension on Platforms { + /// Convert [Platforms] to String + String get asString { + switch (this) { + case Platforms.android: + return _platformAndroid; + case Platforms.iOS: + return _platformIOS; + } + } + + /// Get [Platforms] instance From String + static Platforms fromString(String platform) { + switch (platform) { + case _platformIOS: + return Platforms.iOS; + case _platformAndroid: + return Platforms.android; + default: + throw Exception( + 'Platforms.fromString() $platform not a valid platform type.'); + } + } +} + +/// Get Current Platform +String? getPlatform() { + if (Platform.isAndroid) { + return _platformAndroid; + } else if (Platform.isIOS) { + return _platformIOS; + } + return null; +} + +const String _platformAndroid = 'android'; +const String _platformIOS = 'iOS'; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/properties.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/properties.dart new file mode 100644 index 00000000..cad03340 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/properties.dart @@ -0,0 +1,105 @@ +import 'geo_location.dart'; + +/// Helper class to track event attributes. +class MoEProperties { + /// [MoEProperties] Constructor + MoEProperties() + : generalAttributes = {}, + locationAttributes = {}, + dateTimeAttributes = {}, + isNonInteractive = false; + + /// General Attribute + Map generalAttributes; + + /// Location Attribute + Map> locationAttributes; + + /// Date Time Attributes + Map dateTimeAttributes; + + /// Non Interactive Event Flag + bool isNonInteractive; + + /// Adds an event attribute of type string, number or boolean. + MoEProperties addAttribute(String key, dynamic value) { + if (!_isAcceptedDataType(value)) { + return this; + } + _addAttribute(key, value); + return this; + } + + /// Adds an event attribute of type Date. + /// Date should be in the following format - yyyy-MM-dd'T'HH:mm:ss.fff'Z' + MoEProperties addISODateTime(String key, String value) { + if (_isAttributeNameEmpty(key)) { + return this; + } + dateTimeAttributes.putIfAbsent(key, () => value); + return this; + } + + /// Marks an event as non-interactive. + MoEProperties setNonInteractiveEvent() { + isNonInteractive = true; + return this; + } + + /// Get Event Attributes [Map] + Map getEventAttributeJson() { + return { + _keyEventAttributes: { + _keyGeneralAttributes: generalAttributes, + _keyLocationAttributes: locationAttributes, + _keyDateTimeAttributes: dateTimeAttributes + }, + _keyIsNonInteractive: isNonInteractive + }; + } + + bool _isAttributeNameEmpty(String name) { + return name.isEmpty; + } + + bool _isAcceptedDataType(dynamic attributeType) { + return attributeType is String || + attributeType is int || + attributeType is double || + attributeType is bool || + attributeType is MoEGeoLocation || + attributeType is List; + } + + bool _isAcceptedArrayType(dynamic attributeType) { + return attributeType is String || + attributeType is int || + attributeType is double; + } + + void _addAttribute(String key, dynamic value) { + if (_isAttributeNameEmpty(key)) { + return; + } + if (value is String || value is int || value is double || value is bool) { + generalAttributes.putIfAbsent(key, () => value); + } else if (value is MoEGeoLocation) { + locationAttributes.putIfAbsent(key, () => value.toMap()); + } else if (value is List) { + final List typeCheckedArray = []; + for (final dynamic val in value) { + if (!_isAcceptedArrayType(val)) { + continue; + } + typeCheckedArray.add(val); + } + generalAttributes.putIfAbsent(key, () => typeCheckedArray); + } + } + + final String _keyEventAttributes = 'eventAttributes'; + final String _keyGeneralAttributes = 'generalAttributes'; + final String _keyLocationAttributes = 'locationAttributes'; + final String _keyDateTimeAttributes = 'dateTimeAttributes'; + final String _keyIsNonInteractive = 'isNonInteractive'; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/moe_push_service.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/moe_push_service.dart new file mode 100644 index 00000000..6b838efc --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/moe_push_service.dart @@ -0,0 +1,59 @@ +import '../../internal/constants.dart'; +import '../../internal/logger.dart'; + +/// Type of Push Notification Services +enum MoEPushService { + /// Apple Push Notification Service + apns, + + /// Firebase Cloud Messaging + fcm, + + /// Huawei Push Kit + push_kit, + + /// Xiaomi Push Service + mi_push +} + +String _tag = '${TAG}MoEPushService'; + +/// For Converting [MoEPushService] Enum to String +extension MoEPushServiceExtention on MoEPushService { + /// Convert [MoEPushService] to [String] + String get asString { + switch (this) { + case MoEPushService.apns: + return _pushServiceAPNS; + case MoEPushService.fcm: + return _pushServiceFCM; + case MoEPushService.push_kit: + return _pushServicePushKit; + case MoEPushService.mi_push: + return _pushServiceMiPush; + } + } + + /// [MoEPushService] From string + static MoEPushService fromString(String pushService) { + Logger.v('$_tag fromString() : pushService: $pushService'); + switch (pushService.toUpperCase()) { + case _pushServiceAPNS: + return MoEPushService.apns; + case _pushServiceFCM: + return MoEPushService.fcm; + case _pushServicePushKit: + return MoEPushService.push_kit; + case _pushServiceMiPush: + return MoEPushService.mi_push; + default: + throw Exception( + 'error: MoEPushService.fromString() : $pushService is not a valid pushService type.'); + } + } +} + +const String _pushServiceAPNS = 'APNS'; +const String _pushServiceFCM = 'FCM'; +const String _pushServicePushKit = 'PUSH_KIT'; +const String _pushServiceMiPush = 'MI_PUSH'; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_campaign.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_campaign.dart new file mode 100644 index 00000000..3f263b23 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_campaign.dart @@ -0,0 +1,36 @@ +import '../../internal/constants.dart'; + +/// Push Campaign Related Data +class PushCampaign { + /// [PushCampaign] Constructor + PushCampaign(this.isDefaultAction, this.clickedAction, this.payload, + this.selfHandledPushRedirection); + + /// Default Push Click Action + bool isDefaultAction; + + /// Click Action Data + Map clickedAction; + + /// Campaign payload + Map payload; + + /// If true, MoEngage SDK will not handle Push redirection for Screen name + /// and DeepLinking Push Notifications. Client should handle redirection. + bool selfHandledPushRedirection = false; + + /// To Convert [PushCampaign] to [Map] + Map toMap() { + return { + keyIsDefaultAction: isDefaultAction, + keyClickedAction: clickedAction, + keyPayload: payload, + keySelfHandledPushRedirection: selfHandledPushRedirection + }; + } + + @override + String toString() { + return '{\nisDefaultAction: $isDefaultAction\nclickedAction: $clickedAction\npayload: $payload\nselfHandledPushRedirection: $selfHandledPushRedirection \n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_campaign_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_campaign_data.dart new file mode 100644 index 00000000..221a2679 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_campaign_data.dart @@ -0,0 +1,23 @@ +import './push_campaign.dart'; +import '../account_meta.dart'; +import '../platforms.dart'; + +/// Push Campaign Related Data +class PushCampaignData { + /// [PushCampaignData] Constructor + PushCampaignData(this.platform, this.accountMeta, this.data); + + /// Type of Platform [Android/IOS] + Platforms platform; + + /// Instance of [AccountMeta] + AccountMeta accountMeta; + + /// [PushCampaign] Data + PushCampaign data; + + @override + String toString() { + return '{\nplatform: ${platform.asString}\naccountMeta: $accountMeta\ndata: $data\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_config.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_config.dart new file mode 100644 index 00000000..9ccf661f --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_config.dart @@ -0,0 +1,13 @@ +/// Push Notification Config +class PushConfig { + /// [PushConfig] Constructor + PushConfig({required this.shouldDeliverCallbackOnForegroundClick}); + + /// [PushConfig] Default Named Constructor + PushConfig.defaultConfig() + : this(shouldDeliverCallbackOnForegroundClick: false); + + /// If [shouldDeliverCallbackOnForegroundClick] is true, when push notification + /// is clicked on app foreground, MoEngage SDK will not handle the redirection. + bool shouldDeliverCallbackOnForegroundClick; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_token_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_token_data.dart new file mode 100644 index 00000000..95a0e60b --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/push/push_token_data.dart @@ -0,0 +1,22 @@ +import '../platforms.dart'; +import 'moe_push_service.dart'; + +/// Push Token Data +class PushTokenData { + /// Push Token Related Data + PushTokenData(this.platform, this.token, this.pushService); + + /// Type of Platform [Android/IOS] + Platforms platform; + + /// Push Token + String token; + + /// Type of Push Service + MoEPushService pushService; + + @override + String toString() { + return '{\nplatform: $platform\ntoken: $token\npushService: $pushService\n}'; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/user_deletion_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/user_deletion_data.dart new file mode 100644 index 00000000..18d0fe81 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/model/user_deletion_data.dart @@ -0,0 +1,17 @@ +import 'account_meta.dart'; + +/// User Deletion Data +/// @author Gowtham KK +/// @since 1.1.0 +class UserDeletionData { + /// Constructor for User Deletion Data + UserDeletionData({required this.accountMeta, required this.isSuccess}); + + /// Instance of [AccountMeta] + /// @since 1.1.0 + AccountMeta accountMeta; + + /// True if the user is deleted successfully, else false. + /// @since 1.1.0 + bool isSuccess; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/moengage_flutter_platform_interface.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/moengage_flutter_platform_interface.dart new file mode 100644 index 00000000..d36c9a6d --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/moengage_flutter_platform_interface.dart @@ -0,0 +1,212 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../src/model/app_status.dart'; +import '../src/model/gender.dart'; +import '../src/model/geo_location.dart'; +import '../src/model/moe_init_config.dart'; +import '../src/model/permission_type.dart'; +import '../src/model/properties.dart'; +import '../src/model/push/moe_push_service.dart'; +import 'internal/method_channel_moengage_flutter.dart'; +import 'model/user_deletion_data.dart'; + +/// Platform Interface for MoEngage Flutter Plugin +abstract class MoEngageFlutterPlatform extends PlatformInterface { + /// [MoEngageFlutterPlatform] Constructor + MoEngageFlutterPlatform() : super(token: _token); + + static final Object _token = Object(); + + static MoEngageFlutterPlatform _instance = MethodChannelMoEngageFlutter(); + + /// Returns instance of [MoEngageFlutterPlatform] to use. + /// Defaults to [MethodChannelMoEngageFlutter]. + static MoEngageFlutterPlatform get instance => _instance; + + static set instance(MoEngageFlutterPlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Initialize the MoEngage SDK + /// [moEInitConfig] - Instance of [MoEInitConfig] + /// [appId] - MoEngage App ID + void initialise(MoEInitConfig moEInitConfig, String appId); + + /// Track user behaviour as events with properties + /// [eventName] - Name of the Event to be tracked + /// [eventAttributes] - Instance of [MoEProperties] + /// [appId] - MoEngage App ID + void trackEvent( + String eventName, MoEProperties eventAttributes, String appId); + + /// Set an Unique Identifier for the user + /// [uniqueId] - Unique User Id of type [String] + /// [appId] - MoEngage App ID + void setUniqueId(String uniqueId, String appId); + + /// Update user's unique id which was previously set by setUniqueId(). + /// [newUniqueId] - New Unique Id of the user + /// [appId] - MoEngage App ID + void setAlias(String newUniqueId, String appId); + + /// Tracks user-name as a user attribute. + /// [userName] - User Name Attribute + /// [appId] - Full Name value passed by user + void setUserName(String userName, String appId); + + /// Tracks first name as a user attribute. + /// [firstName] - First Name of the User + /// [appId] - MoEngage App ID + void setFirstName(String firstName, String appId); + + /// Tracks last name as a user attribute. + /// [lastName] - Last Name of the User + /// [appId] - MoEngage App ID + void setLastName(String lastName, String appId); + + /// Tracks user's email-id as a user attribute. + /// [emailId] - Email Id of the User + /// [appId] - MoEngage App ID + void setEmail(String emailId, String appId); + + /// Tracks phone number as a user attribute. + /// [phoneNumber] - Phone Number of the User + /// [appId] - MoEngage App ID + void setPhoneNumber(String phoneNumber, String appId); + + /// Tracks gender as a user attribute. + /// [gender] - Instance of [MoEGender] + /// [appId] - MoEngage App ID + void setGender(MoEGender gender, String appId); + + /// Set's user's location + /// [location] - Instance of [MoEGeoLocation] + /// [appId] - MoEngage App ID + void setLocation(MoEGeoLocation location, String appId); + + /// Set user's birth-date. + /// Birthdate should be sent in the following format - yyyy-MM-dd'T'HH:mm:ss.fff'Z' + /// [birthDate] - ISO Formatted Date String + /// [appId] - MoEngage App ID + void setBirthDate(String birthDate, String appId); + + /// Tracks a user attribute. + /// [userAttributeValue] - Data of type [dynamic] + /// [userAttributeName] - Name of User Attribute + /// [appId] - MoEngage App ID + void setUserAttribute( + String userAttributeName, dynamic userAttributeValue, String appId); + + /// Tracks the given time as user-attribute.
+ /// Date should be passed in the following format - yyyy-MM-dd'T'HH:mm:ss.fff'Z' + /// [userAttributeName] - Name of User Attribute + /// [isoDateString] - ISO Formatted Date String + /// [appId] - MoEngage App ID + void setUserAttributeIsoDate( + String userAttributeName, String isoDateString, String appId); + + /// Tracks the given location as user attribute. + /// [userAttributeName] - Name of User Attribute + /// [location] - Instance of [MoEGeoLocation] + /// [appId] - MoEngage App ID + void setUserAttributeLocation( + String userAttributeName, MoEGeoLocation location, String appId); + + /// This API tells the SDK whether it is a fresh install or an existing application was updated. + /// [appStatus] - Instance of [MoEAppStatus] + /// [appId] - MoEngage App ID + void setAppStatus(MoEAppStatus appStatus, String appId); + + /// Try to show an InApp Message. + /// [appId] - MoEngage App ID + void showInApp(String appId); + + /// Invalidates the existing user and session. A new user + /// and session is created. + /// [appId] - MoEngage App ID + void logout(String appId); + + /// Try to return a self handled in-app to the callback listener. + /// Ensure self handled in-app listener is set before you call this. + /// [appId] - MoEngage App ID + void getSelfHandledInApp(String appId); + + /// Self Handled InApp Action Callback + /// [appId] - MoEngage App ID + /// [payload] - SelfHandled InApp Payload of type [Map] + void selfHandledCallback(Map payload); + + ///Set the current context for the given user for InApps + /// [appId] - MoEngage App ID + /// [contexts] - [List] of Context + void setCurrentContext(List contexts, String appId); + + /// Reset Current Context for InApps + /// [appId] - MoEngage App ID + void resetCurrentContext(String appId); + + /// Pass Push Token To Native SDK + /// [appId] - MoEngage App ID + /// [pushService] - Type of [MoEPushService] + /// [pushToken] - Device Push Token + void passPushToken( + String pushToken, MoEPushService pushService, String appId); + + ///Opt Out Data Tracking + /// [appId] - MoEngage App ID + /// [optOutDataTracking] - Data Tracking OptOut State + void optOutDataTracking(bool optOutDataTracking, String appId); + + /// Push Notification Registration + /// [appId] - MoEngage App ID + void registerForPushNotification(); + + /// Pass Push Payload to the MoEngage SDK. + /// [appId] - MoEngage App ID + /// [payload] - Push Payload Data + /// [pushService] - Type of [MoEPushService] + void passPushPayload( + Map payload, MoEPushService pushService, String appId); + + /// Update SDK State + /// [appId] - MoEngage App ID + /// [shouldEnableSdk] - SDK Enable State + void updateSdkState(bool shouldEnableSdk, String appId); + + /// To be called when Orientation of the App Is Changed + void onOrientationChanged(); + + /// Update Device tracking status for the identifier type + /// [appId] - MoEngage App ID + /// [identifierType] - Type of Identifier + /// [state] - [Bool] value for Enable/Disable State + void updateDeviceIdentifierTrackingStatus( + String appId, String identifierType, bool state); + + ///API to create notification channels on Android. + void setupNotificationChannel(); + + /// Notify the SDK on notification permission granted to the application. + /// [isGranted] - Push Permission Granted Flag + /// [type] - Type of [PermissionType] + void permissionResponse(bool isGranted, PermissionType type); + + /// Navigates the user to the Notification settings + void navigateToSettings(); + + /// Requests the push notification permission + void requestPushPermission(); + + /// Update Push Permission Request Count + /// [requestCount] This count will be incremented to existing value + /// [appId] - MoEngage App ID + void updatePushPermissionRequestCountAndroid(int requestCount, String appId); + + /// Delete User Data from MoEngage Server + /// [appId] - MoEngage App ID + /// @returns - Instance of [Future] of type [UserDeletionData] + /// @since 1.1.0 + Future deleteUser(String appId) => + throw UnimplementedError('deleteUser() not implemented for Platform'); +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/data_payload_mapper.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/data_payload_mapper.dart new file mode 100644 index 00000000..22ccccf1 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/data_payload_mapper.dart @@ -0,0 +1,35 @@ +import '../internal/constants.dart'; +import '../model/properties.dart'; +import 'utils.dart'; + +/// Get Event Tracking Payload +Map getEventPayload( + String eventName, MoEProperties eventAttributes, String appId) { + final Map payload = getAccountMeta(appId); + final Map data = eventAttributes.getEventAttributeJson(); + data[keyEventName] = eventName; + payload[keyData] = data; + return payload; +} + +/// Get User Attribute Payload +Map getUserAttributePayload(String attributeName, + String attributeType, dynamic attributeValue, String appId) { + final Map payload = getAccountMeta(appId); + Map data; + if (attributeType == userAttrTypeLocation) { + data = { + keyAttributeName: attributeName, + keyType: attributeType, + keyLocationAttribute: attributeValue + }; + } else { + data = { + keyAttributeName: attributeName, + keyType: attributeType, + keyAttributeValue: attributeValue + }; + } + payload[keyData] = data; + return payload; +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/in_app_payload_mapper.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/in_app_payload_mapper.dart new file mode 100644 index 00000000..ee2ea0c8 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/in_app_payload_mapper.dart @@ -0,0 +1,168 @@ +import 'dart:convert'; + +import '../internal/constants.dart'; +import '../internal/logger.dart'; +import '../model/inapp/action.dart'; +import '../model/inapp/campaign_context.dart'; +import '../model/inapp/campaign_data.dart'; +import '../model/inapp/click_data.dart'; +import '../model/inapp/inapp_action_type.dart'; +import '../model/inapp/inapp_custom_action.dart'; +import '../model/inapp/inapp_data.dart'; +import '../model/inapp/navigation_action.dart'; +import '../model/inapp/navigation_type.dart'; +import '../model/inapp/self_handled_campaign.dart'; +import '../model/inapp/self_handled_data.dart'; +import '../model/platforms.dart'; +import 'utils.dart'; + +/// InApp Json Payload Mapper +class InAppPayloadMapper { + final String _tag = '${TAG}InAppPayloadMapper'; + + /// Get [SelfHandledCampaignData] from JSON String + SelfHandledCampaignData? selfHandledCampaignFromJson(dynamic methodCallArgs) { + try { + final Map selfHandledPayload = + json.decode(methodCallArgs.toString()) as Map; + final Map data = + selfHandledPayload[keyData] as Map; + return SelfHandledCampaignData( + campaignDataFromMap(data), + accountMetaFromMap( + selfHandledPayload[keyAccountMeta] as Map), + selfHandledCampaignFromMap( + data[keySelfHandled] as Map), + PlatformsExtension.fromString(data[keyPlatform].toString())); + } catch (e, stackTrace) { + Logger.e('$_tag Error: selfHandledCampaignFromJson() :', + error: e, stackTrace: stackTrace); + } + return null; + } + + /// Get [InAppData] from JSON String + InAppData? inAppDataFromJson(dynamic methodCallArgs) { + try { + final Map inAppDataPayload = + json.decode(methodCallArgs.toString()) as Map; + final Map data = + inAppDataPayload[keyData] as Map; + return InAppData( + PlatformsExtension.fromString(data[keyPlatform].toString()), + accountMetaFromMap( + inAppDataPayload[keyAccountMeta] as Map), + campaignDataFromMap(data)); + } catch (e, stackTrace) { + Logger.e('$_tag Error: inAppDataFromJson() :', + error: e, stackTrace: stackTrace); + } + return null; + } + + /// Get [ClickData] from JSON String + ClickData? actionFromJson(dynamic payload) { + try { + Logger.i('$_tag actionFromJson() : $payload'); + + final Map actionPayload = + json.decode(payload.toString()) as Map; + final Map actionData = + actionPayload[keyData] as Map; + return ClickData( + PlatformsExtension.fromString(actionData[keyPlatform].toString()), + accountMetaFromMap( + actionPayload[keyAccountMeta] as Map), + campaignDataFromMap(actionData), + actionFromMap(actionData)); + } catch (e, stackTrace) { + Logger.e('$_tag Error: actionFromJson() :', + error: e, stackTrace: stackTrace); + } + return null; + } + + /// Get [Action] from [Map] + Action actionFromMap(Map actionData) { + switch (actionData[keyActionType]) { + case _actionNavigation: + return navigationActionFromMap( + actionData[keyNavigation] as Map); + case _actionCustomAction: + return customActionFromMap( + actionData[keyCustomAction] as Map); + default: + throw Exception('${actionData[keyActionType]} is not a valid action'); + } + } + + /// Get [NavigationAction] from [Map] + NavigationAction navigationActionFromMap(Map actionData) { + return NavigationAction( + ActionType.navigation, + NavigationTypeExtension.fromString( + actionData[keyNavigationType].toString()), + actionData[keyValue].toString(), + actionData[keyKvPair] as Map); + } + + /// Get [CustomAction] from [Map] + CustomAction customActionFromMap(Map actionData) { + return CustomAction(ActionType.custom, + castOrFallback(actionData[keyKvPair], {})); + } + + /// Get [CampaignData] from [Map] + CampaignData campaignDataFromMap(Map dataPayload) { + return CampaignData( + dataPayload[keyCampaignId].toString(), + dataPayload[keyCampaignName].toString(), + campaignContextFromMap(castOrFallback( + dataPayload[keyCampaignContext], {}))); + } + + /// Get [CampaignContext] from [Map] + CampaignContext campaignContextFromMap(Map dataPayload) { + return CampaignContext( + dataPayload[keyFormattedCampaignId].toString(), dataPayload); + } + + /// Get [SelfHandledCampaign] from [Map] + SelfHandledCampaign selfHandledCampaignFromMap( + Map dataPayload) { + return SelfHandledCampaign( + dataPayload[keyPayload].toString(), + (dataPayload.containsKey(keyDismissInterval) + ? dataPayload[keyDismissInterval] + : -1) as int); + } + + /// Convert [SelfHandledCampaign] to [Map] for given ActionType + Map selfHandleCampaignDataToMap( + SelfHandledCampaignData campaignData, String actionType) { + final Map payload = + accountMetaToMap(campaignData.accountMeta); + payload[keyData] = { + keyType: actionType, + keyCampaignName: campaignData.campaignData.campaignName, + keyCampaignId: campaignData.campaignData.campaignId, + keyCampaignContext: campaignData.campaignData.context.attributes, + keySelfHandled: selfHandleCampaignToMap(campaignData.campaign), + keyPlatform: getPlatform() + }; + + return payload; + } + + /// Convert [SelfHandledCampaign] to [Map] + Map selfHandleCampaignToMap( + SelfHandledCampaign selfHandledCampaign) { + return { + keyPayload: selfHandledCampaign.payload, + keyDismissInterval: selfHandledCampaign.dismissInterval + }; + } +} + +const String _actionNavigation = 'navigation'; +const String _actionCustomAction = 'customAction'; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/init_config_payload_mapper.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/init_config_payload_mapper.dart new file mode 100644 index 00000000..9530d7f2 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/init_config_payload_mapper.dart @@ -0,0 +1,29 @@ +import '../../src/model/moe_init_config.dart'; +import '../../src/model/push/push_config.dart'; +import '../../src/utils/utils.dart'; +import '../internal/constants.dart'; + +/// PayloadMapper for [MoEInitConfig] +class InitConfigPayloadMapper { + /// Convert [MoEInitConfig] to [Map] with Given [appId] + Map getInitPayload( + String appId, MoEInitConfig moEInitConfig) { + final Map payload = getAccountMeta(appId); + payload[keyInitConfig] = _initConfigToMap(moEInitConfig); + return payload; + } + + Map _initConfigToMap(MoEInitConfig moEInitConfig) { + final Map initPayload = {}; + initPayload[keyPushConfig] = _pushConfigToMap(moEInitConfig.pushConfig); + return initPayload; + } + + Map _pushConfigToMap(PushConfig pushConfig) { + final Map pushConfigPayload = { + keyShouldDeliverCallbackOnForegroundClick: + pushConfig.shouldDeliverCallbackOnForegroundClick + }; + return pushConfigPayload; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/push_payload_mapper.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/push_payload_mapper.dart new file mode 100644 index 00000000..1e2560fe --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/push_payload_mapper.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; + +import '../internal/constants.dart'; +import '../internal/logger.dart'; +import '../model/platforms.dart'; +import '../model/push/moe_push_service.dart'; +import '../model/push/push_campaign.dart'; +import '../model/push/push_campaign_data.dart'; +import '../model/push/push_token_data.dart'; +import 'utils.dart'; + +/// InApp Json Payload Mapper +class PushPayloadMapper { + final String _tag = '${TAG}PushPayloadMapper'; + + /// Get [PushTokenData] from Json String + PushTokenData? pushTokenFromJson(dynamic methodCallArgs) { + try { + final Map tokenData = + json.decode(methodCallArgs.toString()) as Map; + return PushTokenData( + PlatformsExtension.fromString(tokenData[keyPlatform].toString()), + tokenData[keyPushToken].toString(), + MoEPushServiceExtention.fromString( + tokenData[keyPushService].toString())); + } catch (exception, stackTrace) { + Logger.e('$_tag Error: pushTokenFromJson() : ', + stackTrace: stackTrace, error: exception); + } + return null; + } + + /// Get [PushCampaignData] from Json String + PushCampaignData? pushCampaignFromJson(dynamic methodCallArgs) { + try { + Logger.v('$_tag pushCampaignFromJson() : $methodCallArgs'); + final Map pushCampaignPayload = + json.decode(methodCallArgs.toString()) as Map; + final Map campaignData = + pushCampaignPayload[keyData] as Map; + return PushCampaignData( + PlatformsExtension.fromString(campaignData[keyPlatform].toString()), + accountMetaFromMap( + pushCampaignPayload[keyAccountMeta] as Map), + PushCampaign( + (campaignData.containsKey(keyIsDefaultAction) + ? campaignData[keyIsDefaultAction] + : false) as bool, + (campaignData.containsKey(keyClickedAction) + ? campaignData[keyClickedAction] + : {}) as Map, + (campaignData.containsKey(keyPayload) + ? campaignData[keyPayload] + : {}) as Map, + (campaignData.containsKey(keySelfHandledPushRedirection) + ? campaignData[keySelfHandledPushRedirection] + : false) as bool)); + } catch (e, stackTrace) { + Logger.e('$_tag Error: pushCampaignFromJson() : ', + stackTrace: stackTrace, error: e); + } + return null; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/utils.dart b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/utils.dart new file mode 100644 index 00000000..58920a60 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/lib/src/utils/utils.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; + +import '../internal/constants.dart'; +import '../model/account_meta.dart'; +import '../model/app_status.dart'; +import '../model/permission_result.dart'; +import '../model/permission_type.dart'; +import '../model/platforms.dart'; + +/// Get Data Tracking OptOut Payload +Map getOptOutTrackingPayload( + String type, bool shouldOptOutDataTracking, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = { + keyType: type, + keyState: shouldOptOutDataTracking + }; + return payload; +} + +/// Get Update SDK state Payload +Map getUpdateSdkStatePayload( + bool shouldEnableSdk, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = getMap(keyIsSdkEnabled, shouldEnableSdk); + return payload; +} + +/// Get [Map] from Key-Value Pair +Map getMap(String key, dynamic value) { + return {key: value}; +} + +/// Get Account Meta for given [appId] +Map getAccountMeta(String appId) { + return { + keyAccountMeta: {keyAppId: appId} + }; +} + +/// Get Alias payload for given [appId] +Map getAliasPayload(String alias, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = getMap(keyAlias, alias); + return payload; +} + +/// Get App Status payload for given [appId] +Map getAppStatusPayload(MoEAppStatus appStatus, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = getMap(keyAppStatus, + appStatus == MoEAppStatus.install ? appStatusInstall : appStatusUpdate); + return payload; +} + +/// Get InApp Context payload for given [appId] +Map getInAppContextPayload( + List contexts, String appId) { + final Map payload = getAccountMeta(appId); + payload[keyData] = {keyContexts: contexts}; + return payload; +} + +/// Get [AccountMeta] from [Map] +AccountMeta accountMetaFromMap(Map metaPayload) { + return AccountMeta(metaPayload[keyAppId].toString()); +} + +/// Convert [AccountMeta] to [Map] +Map accountMetaToMap(AccountMeta accountMeta) { + return getAccountMeta(accountMeta.appId); +} + +/// Get [PermissionResultData] from Json String +PermissionResultData permissionResultFromMap(dynamic methodCallArgs) { + final Map permissionPayload = + json.decode(methodCallArgs.toString()) as Map; + return PermissionResultData( + PlatformsExtension.fromString(permissionPayload[keyPlatform].toString()), + permissionPayload[keyIsPermissionGranted] as bool, + PermissionTypeExtension.fromString( + permissionPayload[keyPermissionType].toString())); +} + +/// Get Permission Response Payload +Map getPermissionResponsePayload( + bool isGranted, PermissionType type) { + return {keyPermissionType: type.asString, keyIsPermissionGranted: isGranted}; +} + +/// Null Safe Type Casting With FallBack +T castOrFallback(dynamic x, T fallback) => x is T ? x : fallback; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/pubspec.yaml b/flutter-sample/core/moengage_flutter_platform_interface/pubspec.yaml new file mode 100644 index 00000000..a4571940 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/pubspec.yaml @@ -0,0 +1,18 @@ +name: moengage_flutter_platform_interface +description: A common platform interface for the moengage_flutter plugin. +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + collection: ^1.16.0 + flutter_test: + sdk: flutter diff --git a/flutter-sample/core/moengage_flutter_platform_interface/test/compator.dart b/flutter-sample/core/moengage_flutter_platform_interface/test/compator.dart new file mode 100644 index 00000000..d1176b81 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/test/compator.dart @@ -0,0 +1,73 @@ +import 'package:collection/collection.dart'; +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; + +class Comparator { + bool isPushTokenDataEqual(PushTokenData? data1, PushTokenData? data2) { + return data1?.platform == data2?.platform && + data1?.pushService == data2?.pushService && + data1?.token == data1?.token; + } + + bool isPushCampaignDataEqual( + PushCampaignData? data1, PushCampaignData? data2) { + return data1?.platform == data2?.platform && + isAccountMetaEqual(data1?.accountMeta, data2?.accountMeta) && + isPushDataEqual(data1?.data, data2?.data); + } + + bool isAccountMetaEqual(AccountMeta? data1, AccountMeta? data2) { + return data1?.appId == data2?.appId; + } + + bool isPushDataEqual(PushCampaign? data1, PushCampaign? data2) { + return data1?.isDefaultAction == data2?.isDefaultAction && + data1?.selfHandledPushRedirection == + data2?.selfHandledPushRedirection && + const DeepCollectionEquality().equals(data1?.payload, data2?.payload) && + const DeepCollectionEquality() + .equals(data2?.clickedAction, data1?.clickedAction); + } + + bool isSelfHandledDataEqual( + SelfHandledCampaignData? data1, SelfHandledCampaignData? data2) { + return isAccountMetaEqual(data1?.accountMeta, data2?.accountMeta) && + data1?.platform == data2?.platform && + isSelfHandledCampaignEqual(data1?.campaign, data2?.campaign) && + isCampaignDataEqual(data1?.campaignData, data2?.campaignData); + } + + bool isSelfHandledCampaignEqual( + SelfHandledCampaign? data1, SelfHandledCampaign? data2) { + return data1?.payload == data2?.payload && + data1?.dismissInterval == data2?.dismissInterval; + } + + bool isCampaignDataEqual(CampaignData? data1, CampaignData? data2) { + return data1?.campaignId == data2?.campaignId && + data1?.campaignName == data2?.campaignName && + isCampaignContextEqual(data1?.context, data2?.context); + } + + bool isCampaignContextEqual(CampaignContext? data1, CampaignContext? data2) { + return data1?.formattedCampaignId == data2?.formattedCampaignId && + const DeepCollectionEquality() + .equals(data1?.attributes, data1?.attributes); + } + + bool isInAppDataEqual(InAppData? data1, InAppData? data2) { + return isAccountMetaEqual(data1?.accountMeta, data2?.accountMeta) && + data1?.platform == data2?.platform && + isCampaignDataEqual(data1?.campaignData, data2?.campaignData); + } + + bool isClickDataEqual(ClickData? data1, ClickData? data2) { + return isAccountMetaEqual(data1?.accountMeta, data2?.accountMeta) && + data1?.platform == data2?.platform && + isCampaignDataEqual(data1?.campaignData, data2?.campaignData) && + isCampaignActionEqual(data1?.action, data2?.action); + } + + bool isCampaignActionEqual(Action? action1, Action? action2) { + return action1?.actionType == action2?.actionType; + } +} diff --git a/flutter-sample/core/moengage_flutter_platform_interface/test/data_provider/data_provider.dart b/flutter-sample/core/moengage_flutter_platform_interface/test/data_provider/data_provider.dart new file mode 100644 index 00000000..8b163c73 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/test/data_provider/data_provider.dart @@ -0,0 +1,94 @@ +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; +import 'package:moengage_flutter_platform_interface/src/model/platforms.dart'; + +final MoEProperties properties = MoEProperties() + .addAttribute('string', 'Mark') + .addAttribute('num', 300) + .addAttribute('double', 60.5) + .addAttribute('bool', false) + .addAttribute('location', MoEGeoLocation(12.1, 77.18)) + .addISODateTime('dateTime', '2019-12-02T08:26:21.170Z') + .setNonInteractiveEvent(); + +final MoEInitConfig moEInitConfig = MoEInitConfig( + pushConfig: PushConfig(shouldDeliverCallbackOnForegroundClick: true)); + +final PushTokenData tokenData = + PushTokenData(Platforms.android, '1234abcd', MoEPushService.fcm); + +final PushCampaignData pushCampaignData = PushCampaignData( + Platforms.android, + AccountMeta(''), + PushCampaign( + true, + {}, + { + 'gcm_image_url': 'https://picsum.photos/200/222', + 'gcm_alert': 'Message', + 'gcm_notificationType': 'gcm_webNotification', + 'push_from': 'moengage', + 'gcm_webUrl': 'monengage://moe_app/add_to_cart', + 'moe_app_id': 'DAO6UGZ73D9RTK8B5W96TPYN_DEBUG', + 'gcm_campaign_id': '000000000000000015056066_L_0', + 'moe_channel_id': 'moe_sound_channel', + 'moe_webUrl': 'monengage://moe_app/add_to_cart?key=value', + 'gcm_subtext': 'Summary', + 'moe_cid_attr': + "{'moe_campaign_channel': 'Push','moe_delivery_type': 'One Time','moe_campaign_id': '000000000000000015056066'}", + 'mi_image_url': 'https://picsum.photos/200/222', + 'MOE_NOTIFICATION_ID': 17987, + 'MOE_MSG_RECEIVED_TIME': 1692068674349, + 'key': 'value', + 'gcm_title': 'Title' + }, + true, + )); + +final SelfHandledCampaignData selfHandledCampaign = SelfHandledCampaignData( + CampaignData( + '64ca642685373efd30dced83', + 'Self handled Test in-app<::>0<::>1', + CampaignContext('64ca642685373efd30dced83_F_T_IA_AB_1_P_0_L_0', { + 'cid': '64ca642685373efd30dced83_F_T_IA_AB_1_P_0_L_0', + 'campaign_name': 'Self handled Test in-app', + 'moe_locale_name': 'Default', + 'moe_locale_id': '0', + 'moe_variation_id': '1' + })), + AccountMeta(''), + SelfHandledCampaign( + '{"key1":"value1","key2":"value2","key3":"value3"}', 60), + Platforms.android); + +final InAppData inAppData = InAppData( + Platforms.android, + AccountMeta(''), + CampaignData( + '64dafbb0fb7525971946fb39', + 'test campaign<::>0<::>1', + CampaignContext('64dafbb0fb7525971946fb39_F_T_IA_AB_1_P_0_L_0', { + 'cid': '64dafbb0fb7525971946fb39_F_T_IA_AB_1_P_0_L_0', + 'campaign_name': 'test campaign', + 'moe_locale_name': 'Default', + 'moe_locale_id': '0', + 'moe_variation_id': '1' + }), + ), +); + +final ClickData clickData = ClickData( + Platforms.android, + AccountMeta(''), + CampaignData( + '64dafbb0fb7525971946fb39', + 'test campaign<::>0<::>1', + CampaignContext('64dafbb0fb7525971946fb39_F_T_IA_AB_1_P_0_L_0', { + 'cid': '64dafbb0fb7525971946fb39_F_T_IA_AB_1_P_0_L_0', + 'campaign_name': 'test campaign', + 'moe_locale_name': 'Default', + 'moe_locale_id': '0', + 'moe_variation_id': '1' + }), + ), + NavigationAction(ActionType.navigation, NavigationType.deeplink, + 'https://google.com', {})); diff --git a/flutter-sample/core/moengage_flutter_platform_interface/test/data_provider/json_data.dart b/flutter-sample/core/moengage_flutter_platform_interface/test/data_provider/json_data.dart new file mode 100644 index 00000000..fc97589b --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/test/data_provider/json_data.dart @@ -0,0 +1,389 @@ +const String initPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "initConfig": { + "pushConfig": { + "shouldDeliverCallbackOnForegroundClick": true + } + } +} +'''; + +const String eventTrackingPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "eventName":"add_to_cart", + "isNonInteractive": true, + "eventAttributes": { + "generalAttributes": { + "string":"Mark", + "num":300, + "double":60.5, + "bool":false + }, + "locationAttributes":{ + "location":{ + "latitude":12.1, + "longitude":77.18 + } + }, + "dateTimeAttributes":{ + "dateTime":"2019-12-02T08:26:21.170Z" + } + } + } +} +'''; + +const String userNamePayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_NAME", + "type":"general", + "attributeValue":"user1234" + } +} +'''; + +const String uniqueIdPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_UNIQUE_ID", + "type":"general", + "attributeValue":"1234" + } +} +'''; + +const String firstNamePayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_FIRST_NAME", + "type":"general", + "attributeValue":"Mark" + } +} +'''; + +const String lastNamePayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_LAST_NAME", + "type":"general", + "attributeValue":"Wood" + } +} +'''; + +const String emailIdPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_EMAIL", + "type":"general", + "attributeValue":"abc@gmail.com" + } +} +'''; + +const String mobileNumberPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_MOBILE", + "type":"general", + "attributeValue":"9876543210" + } +} +'''; + +const String genderPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_GENDER", + "type":"general", + "attributeValue":"female" + } +} +'''; + +const String birthDayPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_BDAY", + "type":"general", + "attributeValue":"2019-12-02T08:26:21.170Z" + } +} +'''; + +const String locationPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"USER_ATTRIBUTE_USER_LOCATION", + "type":"location", + "locationAttribute":{ + "latitude":12.1, + "longitude":77.18 + } + } +} +'''; + +const String userAttrPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"language", + "type":"general", + "attributeValue":"English" + } +} +'''; + +const String userAttrStringArrayPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"string-array", + "type":"general", + "attributeValue":["array","of","strings"] + } +} +'''; + +const String userAttrNumberArrayPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"number-array", + "type":"general", + "attributeValue":[1.5,1,2.56] + } +} +'''; + +const String timeStampPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "attributeName":"timeStamp", + "type":"timestamp", + "attributeValue":"2019-12-02T08:26:21.170Z" + } +} +'''; + +const String aliasPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "alias":"1234" + } +} +'''; + +const String appStatusPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "appStatus":"INSTALL" + } +} +'''; + +const String inAppContextPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "contexts":["home","dashboard"] + } +} +'''; + +const String optOutTrackingPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "type":"data", + "state":false + } +} +'''; + +const String updateSdkStatePayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "isSdkEnabled":false + } +} +'''; + +const String permissionStatePayload = ''' +{ + "type":"push", + "isGranted":false +} +'''; + +const String pushServicePayload = ''' +{ + "platform":"android", + "token":"1234abcd", + "pushService":"fcm" +} +'''; + +const String pushCampaignPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "platform": "android", + "isDefaultAction": true, + "selfHandledPushRedirection": true, + "payload": { + "gcm_image_url": "https://picsum.photos/200/222", + "gcm_alert": "Message", + "gcm_notificationType": "gcm_webNotification", + "push_from": "moengage", + "gcm_webUrl": "monengage://moe_app/add_to_cart", + "moe_app_id": "DAO6UGZ73D9RTK8B5W96TPYN_DEBUG", + "gcm_campaign_id": "000000000000000015056066_L_0", + "moe_channel_id": "moe_sound_channel", + "moe_webUrl": "monengage://moe_app/add_to_cart?key=value", + "gcm_subtext": "Summary", + "moe_cid_attr": "{'moe_campaign_channel': 'Push','moe_delivery_type': 'One Time','moe_campaign_id': '000000000000000015056066'}", + "mi_image_url": "https://picsum.photos/200/222", + "MOE_NOTIFICATION_ID": 17987, + "MOE_MSG_RECEIVED_TIME": 1692068674349, + "key": "value", + "gcm_title": "Title" + } + } +} +'''; + +/// r''' denotes raw string +const String selfHandledPayload = r''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "campaignName": "Self handled Test in-app<::>0<::>1", + "campaignId": "64ca642685373efd30dced83", + "campaignContext": { + "cid": "64ca642685373efd30dced83_F_T_IA_AB_1_P_0_L_0", + "campaign_name": "Self handled Test in-app", + "moe_locale_name": "Default", + "moe_locale_id": "0", + "moe_variation_id": "1" + }, + "platform": "android", + "selfHandled": { + "payload": "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"}", + "dismissInterval": 60 + } + } +} +'''; + +const String inAppPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "campaignName": "test campaign<::>0<::>1", + "campaignId": "64dafbb0fb7525971946fb39", + "campaignContext": { + "cid": "64dafbb0fb7525971946fb39_F_T_IA_AB_1_P_0_L_0", + "campaign_name": "test campaign", + "moe_locale_name": "Default", + "moe_locale_id": "0", + "moe_variation_id": "1" + }, + "platform": "android" + } +} +'''; + +const String inAppClickDataPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "campaignName": "test campaign<::>0<::>1", + "actionType": "navigation", + "navigation":{ + "navigationType":"deep_linking", + "value":"https://google.com", + "kvPair":{} + }, + "campaignId": "64dafbb0fb7525971946fb39", + "campaignContext": { + "cid": "64dafbb0fb7525971946fb39_F_T_IA_AB_1_P_0_L_0", + "campaign_name": "test campaign", + "moe_locale_name": "Default", + "moe_locale_id": "0", + "moe_variation_id": "1" + }, + "platform": "android" + } +} +'''; + +const String inAppClickData = + r'''{"accountMeta":{"appId":""},"data":{"type":"click","campaignName":"Self handled Test in-app<::>0<::>1","campaignId":"64ca642685373efd30dced83","campaignContext":{"cid":"64ca642685373efd30dced83_F_T_IA_AB_1_P_0_L_0","campaign_name":"Self handled Test in-app","moe_locale_name":"Default","moe_locale_id":"0","moe_variation_id":"1"},"selfHandled":{"payload":"{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"}","dismissInterval":60},"platform":null}}'''; diff --git a/flutter-sample/core/moengage_flutter_platform_interface/test/parser_test.dart b/flutter-sample/core/moengage_flutter_platform_interface/test/parser_test.dart new file mode 100644 index 00000000..6c46f433 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_platform_interface/test/parser_test.dart @@ -0,0 +1,142 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; +import 'package:moengage_flutter_platform_interface/src/utils/push_payload_mapper.dart'; +import 'compator.dart'; +import 'data_provider/data_provider.dart'; +import 'data_provider/json_data.dart'; + +void main() { + test('Test Init Payload', () { + expect(InitConfigPayloadMapper().getInitPayload('', moEInitConfig), + jsonDecode(initPayload)); + }); + + test('Test Event Tracking Payload', () { + expect(getEventPayload('add_to_cart', properties, ''), + jsonDecode(eventTrackingPayload)); + }); + + test('Test User Attribute Payload', () { + expect( + getUserAttributePayload( + userAttrNameUserName, 'general', 'user1234', ''), + jsonDecode(userNamePayload)); + expect(getUserAttributePayload(userAttrNameUniqueId, 'general', '1234', ''), + jsonDecode(uniqueIdPayload)); + expect( + getUserAttributePayload(userAttrNameFirstName, 'general', 'Mark', ''), + jsonDecode(firstNamePayload)); + expect(getUserAttributePayload(userAttrNameLastName, 'general', 'Wood', ''), + jsonDecode(lastNamePayload)); + expect( + getUserAttributePayload( + userAttrNameBirtdate, 'general', '2019-12-02T08:26:21.170Z', ''), + jsonDecode(birthDayPayload)); + expect( + getUserAttributePayload( + userAttrNameEmailId, 'general', 'abc@gmail.com', ''), + jsonDecode(emailIdPayload)); + expect( + getUserAttributePayload( + userAttrNamePhoneNum, 'general', '9876543210', ''), + jsonDecode(mobileNumberPayload)); + expect(getUserAttributePayload(userAttrNameGender, 'general', 'female', ''), + jsonDecode(genderPayload)); + expect( + getUserAttributePayload(userAttrNameLocation, 'location', + MoEGeoLocation(12.1, 77.18).toMap(), ''), + jsonDecode(locationPayload)); + expect(getUserAttributePayload('language', 'general', 'English', ''), + jsonDecode(userAttrPayload)); + expect( + getUserAttributePayload( + 'timeStamp', 'timestamp', '2019-12-02T08:26:21.170Z', ''), + jsonDecode(timeStampPayload)); + expect( + getUserAttributePayload( + 'string-array', 'general', ['array', 'of', 'strings'], ''), + jsonDecode(userAttrStringArrayPayload)); + expect( + getUserAttributePayload('number-array', 'general', [1.5, 1, 2.56], ''), + jsonDecode(userAttrNumberArrayPayload)); + }); + + test('Test Alias Payload', () { + expect(getAliasPayload('1234', ''), jsonDecode(aliasPayload)); + }); + + test('Test App Status Payload', () { + expect(getAppStatusPayload(MoEAppStatus.install, ''), + jsonDecode(appStatusPayload)); + }); + + test('Test InApp Context Payload', () { + expect(getInAppContextPayload(['home', 'dashboard'], ''), + jsonDecode(inAppContextPayload)); + }); + + test('Test OptOut Tracking Payload', () { + expect(getOptOutTrackingPayload(gdprOptOutTypeData, false, ''), + jsonDecode(optOutTrackingPayload)); + }); + + test('Test Update SDK State Payload', () { + expect( + getUpdateSdkStatePayload(false, ''), jsonDecode(updateSdkStatePayload)); + }); + + test('Push Permission Payload', () { + expect(getPermissionResponsePayload(false, PermissionType.PUSH), + jsonDecode(permissionStatePayload)); + }); + + test('Push Token Payload', () { + expect( + Comparator().isPushTokenDataEqual( + PushPayloadMapper().pushTokenFromJson(pushServicePayload), + tokenData), + true); + }); + + test('Push Campaign Payload', () { + expect( + Comparator().isPushCampaignDataEqual( + PushPayloadMapper().pushCampaignFromJson(pushCampaignPayload), + pushCampaignData), + true); + }); + + test('InApp Data Action Test', () { + expect( + Comparator().isInAppDataEqual( + InAppPayloadMapper().inAppDataFromJson(inAppPayload), inAppData), + true); + }); + + test('Self Handled InApp Payload', () { + expect( + Comparator().isSelfHandledDataEqual( + InAppPayloadMapper() + .selfHandledCampaignFromJson(selfHandledPayload), + selfHandledCampaign), + true); + }); + + test('Self Handled InApp Payload', () { + expect( + jsonEncode(InAppPayloadMapper() + .selfHandleCampaignDataToMap(selfHandledCampaign, 'click')) == + inAppClickData, + true); + }); + + test('InApp Click Action', () { + expect( + Comparator().isClickDataEqual( + InAppPayloadMapper().actionFromJson(inAppClickDataPayload), + clickData), + true); + }); +} diff --git a/flutter-sample/core/moengage_flutter_web/.gitignore b/flutter-sample/core/moengage_flutter_web/.gitignore new file mode 100644 index 00000000..53e92cc4 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/flutter-sample/core/moengage_flutter_web/CHANGELOG.md b/flutter-sample/core/moengage_flutter_web/CHANGELOG.md new file mode 100644 index 00000000..1d0502ed --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/CHANGELOG.md @@ -0,0 +1,11 @@ +# MoEngage Flutter Web Plugin + +# 07-12-2023 + +## 2.1.0 +- Added support for array in user attributes + +# 13-09-2023 + +## 2.0.0 +- Initial Release with Federated Plugin \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_web/LICENSE b/flutter-sample/core/moengage_flutter_web/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_web/README.md b/flutter-sample/core/moengage_flutter_web/README.md new file mode 100644 index 00000000..2689637b --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/README.md @@ -0,0 +1,15 @@ +# moengage\_flutter\_web + +The Web implementation of [`moengage_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_flutter` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/core/moengage_flutter_web/lib/constants.dart b/flutter-sample/core/moengage_flutter_web/lib/constants.dart new file mode 100644 index 00000000..4d63152d --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/lib/constants.dart @@ -0,0 +1,21 @@ +// ignore_for_file: public_member_api_docs +// Method Names +const String methodInitialise = 'initialise'; +const String methodTrackEvent = 'trackEvent'; +const String methodSetUserAttribute = 'setUserAttribute'; +const String methodSetUserAttributeTimeStamp = 'setUserAttributeTimeStamp'; +const String methodSetAlias = 'setAlias'; +const String methodLogout = 'logout'; + +// SDK Method Names +const String methodTrackEventSDK = 'track_event'; +const String methodSetUserAttributeSDK = 'add_user_attribute'; +const String methodSetAliasSDK = 'update_unique_user_id'; +const String methodLogoutSDK = 'destroy_session'; + +// Keys Constants +String keyEventName = 'eventName'; +String keyEventAttributes = 'eventAttributes'; +String keyAttributeValue = 'attributeValue'; +String keyAttributeName = 'attributeName'; +String keyAlias = 'alias'; diff --git a/flutter-sample/core/moengage_flutter_web/lib/extensions.dart b/flutter-sample/core/moengage_flutter_web/lib/extensions.dart new file mode 100644 index 00000000..ca67717c --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/lib/extensions.dart @@ -0,0 +1,29 @@ +// ignore_for_file: public_member_api_docs + +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart' + show MoEProperties; + +extension MoEEvent on MoEProperties { + Map getNormalizedEventAttributeJson() { + final Map dateTimeAttributesParsed = {}; + dateTimeAttributes.forEach( + (String k, String v) => dateTimeAttributesParsed[k] = { + keyTimeStampWeb: DateTime.parse(v).millisecondsSinceEpoch.toString() + }, + ); + return { + _keyEventAttributes: { + ...generalAttributes, + ...locationAttributes, + ...dateTimeAttributesParsed + }, + _keyIsNonInteractive: isNonInteractive + }; + } +} + +String keyTimeStampWeb = 'dartTimeStamp'; +String keyPayload = 'payload'; +String keyKvPair = 'kvPair'; +String _keyEventAttributes = 'eventAttributes'; +String _keyIsNonInteractive = 'isNonInteractive'; diff --git a/flutter-sample/core/moengage_flutter_web/lib/moengage_flutter_web.dart b/flutter-sample/core/moengage_flutter_web/lib/moengage_flutter_web.dart new file mode 100644 index 00000000..2d4713f3 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/lib/moengage_flutter_web.dart @@ -0,0 +1,248 @@ +import 'dart:js'; +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart' + hide keyAlias, keyEventAttributes, keyEventName; +import 'constants.dart'; +import 'utils.dart'; + +/// The Web implementation of [MoEngageFlutterPlatform]. +class MoEngageFlutterWeb extends MoEngageFlutterPlatform { + /// Registers this class as the default instance of [MoEngageFlutterPlatform] + static void registerWith([Object? registrar]) { + MoEngageFlutterPlatform.instance = MoEngageFlutterWeb(); + } + + JsObject? _moengage; + @override + void initialise(MoEInitConfig moEInitConfig, String appId) { + Logger.d('initialise() : Initialising MoEngage web SDK'); + _moengage = JsObject.fromBrowserObject(context['Moengage'] as Object); + } + + @override + void trackEvent( + String eventName, + MoEProperties eventAttributes, + String appId, + ) { + final Map payload = + getEventPayloadWeb(eventName, eventAttributes); + _moengage?.callMethod(methodTrackEventSDK, [ + payload[keyEventName], + JsObject.jsify(payload[keyEventAttributes] as Object) + ]); + } + + @override + void logout(String appId) { + _moengage?.callMethod(methodLogoutSDK); + } + + @override + void setUserAttribute( + String userAttributeName, + dynamic userAttributeValue, + String appId, + ) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttributeName, getUserAttributeValuePayload(userAttributeValue)], + ); + } + + @override + void setAlias(String newUniqueId, String appId) { + _moengage?.callMethod(methodSetAliasSDK, [newUniqueId]); + } + + @override + void setBirthDate(String birthDate, String appId) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttrNameBirtdate, birthDate], + ); + } + + @override + void setEmail(String emailId, String appId) { + _moengage?.callMethod(methodSetUserAttributeSDK, [userAttrNameEmailId, emailId]); + } + + @override + void setFirstName(String firstName, String appId) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttrNameFirstName, firstName], + ); + } + + @override + void setGender(MoEGender gender, String appId) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttrNameGender, genderToString(gender)], + ); + } + + @override + void setLastName(String lastName, String appId) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttrNameLastName, lastName], + ); + } + + @override + void setPhoneNumber(String phoneNumber, String appId) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttrNamePhoneNum, phoneNumber], + ); + } + + @override + void setUniqueId(String uniqueId, String appId) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttrNameUniqueId, uniqueId], + ); + } + + @override + void setUserName(String userName, String appId) { + _moengage?.callMethod( + methodSetUserAttributeSDK, + [userAttrNameUserName, userName], + ); + } + + @override + void setUserAttributeIsoDate( + String userAttributeName, + String isoDateString, + String appId, + ) { + Logger.v('setUserAttributeIsoDate(): Not supported in Web Platform'); + } + + @override + void setUserAttributeLocation( + String userAttributeName, + MoEGeoLocation location, + String appId, + ) { + Logger.v('setUserAttributeLocation(): Not supported in Web Platform'); + } + + @override + void navigateToSettings() { + Logger.v('navigateToSettings(): Not supported in Web Platform'); + } + + @override + void onOrientationChanged() { + Logger.v('onOrientationChanged(): Not supported in Web Platform'); + } + + @override + void passPushPayload( + Map payload, + MoEPushService pushService, + String appId, + ) { + Logger.v('passPushPayload(): Not supported in Web Platform'); + } + + @override + void passPushToken( + String pushToken, + MoEPushService pushService, + String appId, + ) { + Logger.v('passPushToken(): Not supported in Web Platform'); + } + + @override + void permissionResponse(bool isGranted, PermissionType type) { + Logger.v('permissionResponse(): Not supported in Web Platform'); + } + + @override + void requestPushPermission() { + Logger.v('requestPushPermission(): Not supported in Web Platform'); + } + + @override + void setupNotificationChannel() { + Logger.v('setupNotificationChannel(): Not supported in Web Platform'); + } + + @override + void updateDeviceIdentifierTrackingStatus( + String appId, + String identifierType, + bool state, + ) { + Logger.v( + 'updateDeviceIdentifierTrackingStatus(): Not supported in Web Platform', + ); + } + + @override + void updatePushPermissionRequestCountAndroid(int requestCount, String appId) { + Logger.v( + 'updatePushPermissionRequestCountAndroid(): Not supported in Web Platform', + ); + } + + @override + void getSelfHandledInApp(String appId) { + Logger.v( + 'updatePushPermissionRequestCountAndroid(): Not supported in Web Platform', + ); + } + + @override + void optOutDataTracking(bool optOutDataTracking, String appId) { + Logger.v('optOutDataTracking(): Not supported in Web Platform'); + } + + @override + void registerForPushNotification() { + Logger.v('registerForPushNotification(): Not supported in Web Platform'); + } + + @override + void resetCurrentContext(String appId) { + Logger.v('resetCurrentContext(): Not supported in Web Platform'); + } + + @override + void selfHandledCallback(Map payload) { + Logger.v('selfHandledCallback(): Not supported in Web Platform'); + } + + @override + void setAppStatus(MoEAppStatus appStatus, String appId) { + Logger.v('setAppStatus(): Not supported in Web Platform'); + } + + @override + void setCurrentContext(List contexts, String appId) { + Logger.v('setCurrentContext(): Not supported in Web Platform'); + } + + @override + void setLocation(MoEGeoLocation location, String appId) { + Logger.v('setLocation(): Not supported in Web Platform'); + } + + @override + void showInApp(String appId) { + Logger.v('showInApp(): Not supported in Web Platform'); + } + + @override + void updateSdkState(bool shouldEnableSdk, String appId) { + Logger.v('updateSdkState(): Not supported in Web Platform'); + } +} diff --git a/flutter-sample/core/moengage_flutter_web/lib/utils.dart b/flutter-sample/core/moengage_flutter_web/lib/utils.dart new file mode 100644 index 00000000..63741c4a --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/lib/utils.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs +import 'package:moengage_flutter_platform_interface/moengage_flutter_platform_interface.dart'; +import 'extensions.dart'; +import 'dart:js' as js; + +Map getEventPayloadWeb( + String eventName, + MoEProperties eventAttributes, +) { + final Map eventPayload = + eventAttributes.getNormalizedEventAttributeJson(); + eventPayload[keyEventName] = eventName; + return eventPayload; +} + +Map getUserAttributePayload( + String attributeName, + String attributeType, + dynamic attributeValue, +) { + if (attributeType == userAttrTypeLocation) { + return { + keyAttributeName: attributeName, + keyAttributeType: attributeType, + keyLocationAttribute: attributeValue + }; + } else { + return { + keyAttributeName: attributeName, + keyAttributeType: attributeType, + keyAttributeValue: attributeValue + }; + } +} + +dynamic getUserAttributeValuePayload(dynamic userAttributeValue) { + if (userAttributeValue is List) { + return js.JsArray.from(userAttributeValue); + } + return userAttributeValue; +} diff --git a/flutter-sample/core/moengage_flutter_web/pubspec.yaml b/flutter-sample/core/moengage_flutter_web/pubspec.yaml new file mode 100644 index 00000000..7df14087 --- /dev/null +++ b/flutter-sample/core/moengage_flutter_web/pubspec.yaml @@ -0,0 +1,28 @@ +name: moengage_flutter_web +description: Web implementation of the moengage_flutter plugin +version: 2.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_flutter + platforms: + web: + pluginClass: MoEngageFlutterWeb + fileName: moengage_flutter_web.dart + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + moengage_flutter_platform_interface: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + diff --git a/flutter-sample/example/.gitignore b/flutter-sample/example/.gitignore new file mode 100644 index 00000000..c11d0b0e --- /dev/null +++ b/flutter-sample/example/.gitignore @@ -0,0 +1,79 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/.settings +**/android/app/keystore.properties +**/android/app/moengage123.jks +.gradle + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/flutter-sample/example/.metadata b/flutter-sample/example/.metadata new file mode 100644 index 00000000..fea404f4 --- /dev/null +++ b/flutter-sample/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 68587a0916366e9512a78df22c44163d041dd5f3 + channel: stable + +project_type: app diff --git a/flutter-sample/example/README.md b/flutter-sample/example/README.md new file mode 100644 index 00000000..5a5a6f45 --- /dev/null +++ b/flutter-sample/example/README.md @@ -0,0 +1,62 @@ +# MoEngage Flutter Plugin + +Flutter Plugin for MoEngage Platform + +## SDK Installation + +To add the MoEngage Flutter SDK to your application, edit your application's `pubspec.yaml` file and add the below dependency to it: + +```yaml +dependencies: + moengage_flutter: 1.0.0 +``` + Run flutter packages get to install the SDK. + + ### Android Installation + + ![MavenBadge](https://maven-badges.herokuapp.com/maven-central/com.moengage/moe-android-sdk/badge.svg) + + + Once you install the Flutter Plugin add MoEngage's native Android SDK dependency to the Android project of your application. + Navigate to `android --> app --> build.gradle`. Add the MoEngage Android SDK's dependency in the `dependencies` block + + ```groovy +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation("androidx.core:core:1.3.1") + implementation("androidx.appcompat:appcompat:1.2.0") + implementation("androidx.lifecycle:lifecycle-process:2.2.0") + implementation("com.moengage:moe-android-sdk:$sdkVersion") +} + ``` +where `$sdkVersion` should be replaced by the latest version of the MoEngage SDK. + +## SDK Initialization + +### Android SDK Initialization +Get APP ID from the Settings Page on the MoEngage dashboard and initialize the MoEngage SDK in the `Application` class's `onCreate()` + +```kotlin +// this is the instance of the application class and "XXXXXXXXXXX" is the APP ID from the dashboard. +val moEngage = MoEngage.Builder(this, "XXXXXXXXXXX") + +MoEInitializer.initialize(context, builder) +``` +Refer to the [API reference doc](https://moengage.github.io/android-api-reference/) for a detailed list of possible configurations. + +``` +Note: +All the configuration should be added to the builder before calling initialize. If you are calling initialize at multiple places please ensure that all the required flags and configuration are set each time you initialize to maintain consistency in behavior. +``` + +### iOS SDK Initialization + +Make sure to run `flutter build` command to make sure all the CocoaPods dependencies are added to your project. (i.e, `MoEngage-iOS-SDK` and `moengage_flutter`) + +To initialize the iOS Application with the MoEngage App ID from Settings in Dashboard. In your project, go to AppDelegate file and call the initialize method of `MOFlutterInitializer` instance in applicationdidFinishLaunchingWithOptions() method as shown below: + +```swift + MOFlutterInitializer.sharedInstance.initializeWithAppID("Your App ID", withLaunchOptions: launchOptions) +``` + +Refer to the [Documentation](https://developers.moengage.com/hc/en-us/categories/4404300700308-Flutter-SDK) for complete integration guide. \ No newline at end of file diff --git a/flutter-sample/example/android/.project b/flutter-sample/example/android/.project new file mode 100644 index 00000000..3964dd3f --- /dev/null +++ b/flutter-sample/example/android/.project @@ -0,0 +1,17 @@ + + + android + Project android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/flutter-sample/example/android/app/.classpath b/flutter-sample/example/android/app/.classpath new file mode 100644 index 00000000..eb19361b --- /dev/null +++ b/flutter-sample/example/android/app/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/flutter-sample/example/android/app/.project b/flutter-sample/example/android/app/.project new file mode 100644 index 00000000..ac485d7c --- /dev/null +++ b/flutter-sample/example/android/app/.project @@ -0,0 +1,23 @@ + + + app + Project app created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/flutter-sample/example/android/app/.settings/org.eclipse.buildship.core.prefs b/flutter-sample/example/android/app/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 00000000..b1886adb --- /dev/null +++ b/flutter-sample/example/android/app/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir=.. +eclipse.preferences.version=1 diff --git a/flutter-sample/example/android/app/build.gradle b/flutter-sample/example/android/app/build.gradle new file mode 100644 index 00000000..c7d19dde --- /dev/null +++ b/flutter-sample/example/android/app/build.gradle @@ -0,0 +1,120 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException( + "Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +apply plugin: 'com.huawei.agconnect' +apply plugin: 'kotlin-android' +apply plugin: 'com.google.gms.google-services' +apply plugin: 'kotlin-kapt' + +android { + compileSdk 33 + namespace "com.moengage.sampleapp" + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "com.moengage.sampleapp" + minSdk 21 + targetSdk 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + + signingConfigs { + Properties properties = new Properties() + properties.load(file('keystore.properties').newDataInputStream()) + release { + storeFile file(properties.getProperty("storeFilePath")) + storePassword properties.getProperty("storePassword") + keyAlias properties.getProperty("keyAlias") + keyPassword properties.getProperty("keyPassword") + } + debug { + storeFile file(properties.getProperty("storeFilePath")) + storePassword properties.getProperty("storePassword") + keyAlias properties.getProperty("keyAlias") + keyPassword properties.getProperty("keyPassword") + } + } + + buildTypes { + release { + signingConfig signingConfigs.release + } + debug { + signingConfig signingConfigs.debug + } + } + + repositories { + flatDir { + dirs 'libs' + } + } + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + + kotlinOptions { + jvmTarget = '1.8' + } +} + +flutter { + source '../..' +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', "*.aar"]) + implementation(appLibs.fcm) + implementation(appLibs.lifecycleOwner) + implementation(appLibs.adIdentifier) + implementation(appLibs.hmsPush) + implementation(appLibs.kotlin) + implementation(appLibs.playLocation) + implementation(appLibs.glideCore) + kapt(appLibs.glideCore) + implementation(moengage.core) + implementation(moengage.inapp) + implementation(moengage.cardsCore) + implementation(moengage.pushKit) + implementation(moengage.richNotification) + implementation(moengage.inboxCore) + implementation(moengage.geofence) + implementation(moengage.pushAmpPlus) + testImplementation(appLibs.junit) + androidTestImplementation(appLibs.androidJUnit) + androidTestImplementation(appLibs.expresso) +} + +apply plugin: 'com.google.gms.google-services' diff --git a/flutter-sample/example/android/app/google-services.json b/flutter-sample/example/android/app/google-services.json new file mode 100644 index 00000000..e9f02a26 --- /dev/null +++ b/flutter-sample/example/android/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "615448685370", + "firebase_url": "https://moesampleapp.firebaseio.com", + "project_id": "moesampleapp", + "storage_bucket": "moesampleapp.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:615448685370:android:6d75bcba72eb0cc3", + "android_client_info": { + "package_name": "com.moengage.sampleapp" + } + }, + "oauth_client": [ + { + "client_id": "615448685370-t44jadhvvl6rbquccpmcuftu4a7obfui.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.moengage.sampleapp", + "certificate_hash": "bb66a47ede36191adbca2fd8061aa3ef019a7a00" + } + }, + { + "client_id": "615448685370-bs2ol993hu6u6kq14gsn24uage57mi4u.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCyKHUkkxH537ojYCaRXQ3_pp9EY9Sk88E" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "615448685370-bs2ol993hu6u6kq14gsn24uage57mi4u.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/flutter-sample/example/android/app/libs/MiPush_SDK_Client_5_0_6-G_3rd.aar b/flutter-sample/example/android/app/libs/MiPush_SDK_Client_5_0_6-G_3rd.aar new file mode 100644 index 00000000..aae0ffd8 Binary files /dev/null and b/flutter-sample/example/android/app/libs/MiPush_SDK_Client_5_0_6-G_3rd.aar differ diff --git a/flutter-sample/example/android/app/src/debug/AndroidManifest.xml b/flutter-sample/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..85e23114 --- /dev/null +++ b/flutter-sample/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/flutter-sample/example/android/app/src/main/AndroidManifest.xml b/flutter-sample/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..29666aff --- /dev/null +++ b/flutter-sample/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter-sample/example/android/app/src/main/java/com/moengage/push/amp/plus/MiPushHelper.kt b/flutter-sample/example/android/app/src/main/java/com/moengage/push/amp/plus/MiPushHelper.kt new file mode 100644 index 00000000..5ef3b2fb --- /dev/null +++ b/flutter-sample/example/android/app/src/main/java/com/moengage/push/amp/plus/MiPushHelper.kt @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2014-2022 MoEngage Inc. + * + * All rights reserved. + * + * Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. + * Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. + * Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.moengage.push.amp.plus + +import android.app.ActivityManager +import android.content.Context +import android.os.Process +import com.moengage.core.LogLevel +import com.moengage.core.internal.MANUFACTURER_XIAOMI +import com.moengage.core.internal.global.GlobalResources +import com.moengage.core.internal.logger.Logger +import com.moengage.core.internal.utils.MoEUtils +import com.moengage.core.internal.utils.jsonToBundle +import com.moengage.mi.MoEMiPushHelper +import com.moengage.pushbase.MoEPushHelper +import com.xiaomi.channel.commonutils.android.Region +import com.xiaomi.mipush.sdk.ErrorCode +import com.xiaomi.mipush.sdk.MiPushClient +import com.xiaomi.mipush.sdk.MiPushCommandMessage +import com.xiaomi.mipush.sdk.MiPushMessage +import org.json.JSONObject + +/** + * Helper class to integrate Push-Amp-Plus module of the MoEngage SDK. + * + * @author Umang Chamaria + * @since 1.0.0 + */ +public object MiPushHelper { + + private const val tag = "MiPushHelper" + + /** + * Helper method to pass notification click callback to the MoEngage SDK. + * + * @param context instance of [Context] + * @param message instance of [MiPushMessage] + * @since 1.0.0 + */ + public fun onNotificationClicked(context: Context, message: MiPushMessage) { + try { + Logger.print { "$tag onNotificationClicked() : Notification clicked: $message" } + val messageContent = message.content + if (messageContent.isNullOrBlank()) return + val pushPayload = jsonToBundle(JSONObject(messageContent)) ?: return + MoEMiPushHelper.getInstance().onNotificationClicked(context, pushPayload) + } catch (e: Throwable) { + Logger.print(LogLevel.ERROR, e) { "$tag onNotificationClicked() : " } + } + } + + /** + * Helper method to pass the notification payload to the MoEngage SDK. + * + * @param context instance of [Context] + * @param message instance of [MiPushMessage] + * @since 1.0.0 + */ + public fun passPushPayload(context: Context, message: MiPushMessage) { + try { + Logger.print { "$tag passPushPayload() : $message" } + val messageContent = message.content + if (messageContent.isNullOrBlank()) return + val pushPayload = jsonToBundle(JSONObject(messageContent)) + MoEMiPushHelper.getInstance().passPushPayload(context, pushPayload) + } catch (e: Exception) { + Logger.print(LogLevel.ERROR, e) { "$tag passPushPayload() : " } + } + } + + /** + * Helper method to pass the Push token to the MoEngage SDK. + * + * @param context instance of [Context] + * @param message instance of [MiPushCommandMessage] + * @since 1.0.0 + */ + public fun passPushToken(context: Context, message: MiPushCommandMessage) { + try { + Logger.print { "$tag passPushToken() : Message: $message" } + val command = message.command + if (MiPushClient.COMMAND_REGISTER != command) { + Logger.print { "$tag passPushToken() : Received command is not register command." } + return + } + if (message.resultCode != ErrorCode.SUCCESS.toLong()) { + Logger.print { "$tag passPushToken() : Registration failed." } + return + } + val arguments = message.commandArguments ?: return + val pushToken = if (arguments.size > 0) arguments[0] else null + if (pushToken.isNullOrEmpty()) { + Logger.print { "$tag passPushToken() : Token is null or empty." } + return + } + MoEMiPushHelper.getInstance().passPushToken(context, pushToken) + } catch (e: Throwable) { + Logger.print(LogLevel.ERROR, e) { "$tag passPushToken() : " } + } + } + + /** + * Helper API to check if the payload is from MoEngage platform or not. + * + * @param message instance of [MiPushMessage] + * @since 1.0.0 + */ + public fun isFromMoEngagePlatform(message: MiPushMessage): Boolean { + try { + val messageContent = message.content + if (messageContent.isNullOrBlank()) return false + val payload = jsonToBundle(JSONObject(messageContent)) + return MoEPushHelper.getInstance().isFromMoEngagePlatform(payload) + } catch (e: Throwable) { + Logger.print(LogLevel.ERROR, e) { "$tag isFromMoEngagePlatform() : " } + } + return false + } + + /** + * Initialise Mi SDK + * + * @param context instance of [Context] + * @param appId App-Id from the Mi Dashboard. + * @param appKey App-Key from the Mi Dashboard. + * @param region The region in which the Mi data should reside. Set the region using [Region]. + * + * @since 1.0.0 + */ + public fun initialiseMiPush(context: Context, appKey: String, appId: String, region: Region) { + if (MANUFACTURER_XIAOMI != MoEUtils.deviceManufacturer()) { + Logger.print(LogLevel.WARN) { "$tag initialiseMiPush() : Not a Xiaomi device, rejecting Mi token." } + return + } + if (!MoEMiPushHelper.getInstance().hasMiUi()) { + Logger.print { "$tag initialiseMiPush() : Device Does not have Mi Ui will not register for mi push" } + return + } + setDataRegion(context, region) + initialise(context, appId, appKey, region) + } + + /** + * Set the region in which the Mi data reside. + * + * @param context: instance of [Context] + * @param region: The region in which the Mi data reside. Set the region using [Region]. + * + * @since 1.0.0 + */ + public fun setDataRegion(context: Context, region: Region) { + try { + MoEMiPushHelper.getInstance().setDataRegion(context, region.toString().lowercase()) + } catch (e: Throwable) { + Logger.print(LogLevel.ERROR, e) { "$tag setDataRegion() : " } + } + } + + private fun isMainProcess(context: Context): Boolean { + val am = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager ?: return false + val processInfos = am.runningAppProcesses + val mainProcessName = context.packageName + val myPid = Process.myPid() + for (info in processInfos) { + if (info.pid == myPid && mainProcessName == info.processName) { + return true + } + } + return false + } + + private fun initialise(context: Context, appId: String, appKey: String, region: Region) { + try { + Logger.print { "$tag initialiseMiPush() : Will initialise Mi Push if required." } + Logger.print { "$tag initialiseMiPush(): AppId: $appId AppKey: $appKey" } + if (!isMainProcess(context)) { + Logger.print { "$tag initialiseMiPush() : Will not initialise, not the main process" } + return + } + Logger.print { "$tag initialiseMiPush() : Will register for Mi Push" } + GlobalResources.executor.execute { + MiPushClient.setRegion(region) + MiPushClient.registerPush(context.applicationContext, appId, appKey) + } + } catch (e: Throwable) { + Logger.print(LogLevel.ERROR, e) { "$tag initialiseMiPush() : " } + } + } +} \ No newline at end of file diff --git a/flutter-sample/example/android/app/src/main/java/com/moengage/push/amp/plus/MiPushReceiver.kt b/flutter-sample/example/android/app/src/main/java/com/moengage/push/amp/plus/MiPushReceiver.kt new file mode 100644 index 00000000..f14065dd --- /dev/null +++ b/flutter-sample/example/android/app/src/main/java/com/moengage/push/amp/plus/MiPushReceiver.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014-2022 MoEngage Inc. + * + * All rights reserved. + * + * Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. + * Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. + * Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.moengage.push.amp.plus + +import android.content.Context +import com.moengage.core.internal.logger.Logger +import com.xiaomi.mipush.sdk.MiPushCommandMessage +import com.xiaomi.mipush.sdk.MiPushMessage +import com.xiaomi.mipush.sdk.PushMessageReceiver + +/** + * Callback receiver for Mi SDK + * + * @author Umang Chamaria + * @since 1.0.0 + */ +public class MiPushReceiver : PushMessageReceiver() { + + private val tag = "MiPushReceiver" + + override fun onReceivePassThroughMessage(context: Context?, message: MiPushMessage?) { + Logger.print { "$tag onReceivePassThroughMessage() : $message" } + if (message == null || context == null) return + if (MiPushHelper.isFromMoEngagePlatform(message)) { + MiPushHelper.passPushPayload(context, message) + } + } + + override fun onNotificationMessageClicked(context: Context?, message: MiPushMessage?) { + Logger.print { "$tag onNotificationMessageClicked() : $message" } + if (message == null || context == null) return + MiPushHelper.onNotificationClicked(context, message) + } + + override fun onReceiveRegisterResult(context: Context?, message: MiPushCommandMessage?) { + Logger.print { "$tag onReceiveRegisterResult() : $message" } + if (message == null || context == null) return + MiPushHelper.passPushToken(context, message) + } + + override fun onCommandResult(context: Context?, message: MiPushCommandMessage?) { + Logger.print { "$tag onCommandResult() : $message" } + if (message == null || context == null) return + MiPushHelper.passPushToken(context, message) + } +} \ No newline at end of file diff --git a/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/CustomPushListener.kt b/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/CustomPushListener.kt new file mode 100644 index 00000000..dd12cb3f --- /dev/null +++ b/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/CustomPushListener.kt @@ -0,0 +1,26 @@ +package com.moengage.sampleapp + +import android.app.Activity +import android.content.Context +import android.os.Bundle +import com.moengage.core.internal.logger.Logger +import com.moengage.plugin.base.push.PluginPushCallback + +/** + * @author Umang Chamaria + * Date: 2020/12/06 + */ +class CustomPushListener : PluginPushCallback() { + + private val tag = "CustomPushListener" + override fun handleCustomAction(context: Context, payload: String) { + super.handleCustomAction(context, payload) + Logger.print { "$tag handleCustomAction() : " } + } + + override fun onNotificationClick(activity: Activity, payload: Bundle) { + super.onNotificationClick(activity, payload) + Logger.print { "$tag onNotificationClick() : " } + } + +} \ No newline at end of file diff --git a/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/MainActivity.kt b/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/MainActivity.kt new file mode 100644 index 00000000..1efd15e7 --- /dev/null +++ b/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/MainActivity.kt @@ -0,0 +1,38 @@ +package com.moengage.sampleapp + +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import android.util.Log +import com.moengage.flutter.MoEFlutterHelper.Companion.getInstance +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +class MainActivity : FlutterActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + processIntent(intent) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + Log.d("MainActivity", " : onConfigurationChanged() : ${newConfig.orientation}") + // Checks the orientation of the screen + getInstance().onConfigurationChanged() + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + // GeneratedPluginRegistrant.registerWith(flutterEngine); + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + processIntent(intent) + } + + private fun processIntent(intent: Intent?) { + if (intent == null) return + Log.d("MainActivity", " : processIntent() : ${intent.data}") + } +} \ No newline at end of file diff --git a/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/SampleApplication.kt b/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/SampleApplication.kt new file mode 100644 index 00000000..609521c9 --- /dev/null +++ b/flutter-sample/example/android/app/src/main/java/com/moengage/sampleapp/SampleApplication.kt @@ -0,0 +1,47 @@ +package com.moengage.sampleapp + +import com.moengage.core.LogLevel +import com.moengage.core.MoEngage +import com.moengage.core.config.FcmConfig +import com.moengage.core.config.LogConfig +import com.moengage.core.config.NotificationConfig +import com.moengage.core.config.PushKitConfig +import com.moengage.core.model.SdkState +import com.moengage.flutter.MoEInitializer +import com.moengage.push.amp.plus.MiPushHelper.initialiseMiPush +import com.moengage.pushbase.MoEPushHelper +import com.xiaomi.channel.commonutils.android.Region +import io.flutter.app.FlutterApplication + +/** + * @author Umang Chamaria + * Date: 2019-12-13 + */ +class SampleApplication : FlutterApplication() { + override fun onCreate() { + super.onCreate() + val moEngage: MoEngage.Builder = MoEngage.Builder(this, "DAO6UGZ73D9RTK8B5W96TPYN") + .configureNotificationMetaData( + NotificationConfig( + R.drawable.icon, + R.drawable.ic_launcher, + notificationColor = -1, + isMultipleNotificationInDrawerEnabled = false, + isBuildingBackStackEnabled = true, + isLargeIconDisplayEnabled = true + ) + ) + .configureLogs(LogConfig(LogLevel.VERBOSE, true)) + .configureFcm(FcmConfig(true)) + .configurePushKit(PushKitConfig(true)) + initialiseMiPush( + context = this, + appKey = "5601804211309", + appId = "2882303761518042309", + region = Region.India + ) + MoEInitializer.initialiseDefaultInstance(applicationContext, moEngage, SdkState.ENABLED,true) + // optional, required in-case notification customisation is required. + MoEPushHelper.getInstance().registerMessageListener(CustomPushListener()) + } +} \ No newline at end of file diff --git a/flutter-sample/example/android/app/src/main/res/drawable/ic_launcher.png b/flutter-sample/example/android/app/src/main/res/drawable/ic_launcher.png new file mode 100644 index 00000000..fcc1676a Binary files /dev/null and b/flutter-sample/example/android/app/src/main/res/drawable/ic_launcher.png differ diff --git a/flutter-sample/example/android/app/src/main/res/drawable/icon.png b/flutter-sample/example/android/app/src/main/res/drawable/icon.png new file mode 100644 index 00000000..37c18b7b Binary files /dev/null and b/flutter-sample/example/android/app/src/main/res/drawable/icon.png differ diff --git a/flutter-sample/example/android/app/src/main/res/drawable/launch_background.xml b/flutter-sample/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/flutter-sample/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/flutter-sample/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter-sample/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/flutter-sample/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/flutter-sample/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter-sample/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/flutter-sample/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/flutter-sample/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter-sample/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/flutter-sample/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/flutter-sample/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter-sample/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/flutter-sample/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/flutter-sample/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter-sample/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/flutter-sample/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/flutter-sample/example/android/app/src/main/res/values/styles.xml b/flutter-sample/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..a676f3d8 --- /dev/null +++ b/flutter-sample/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/flutter-sample/example/android/app/src/profile/AndroidManifest.xml b/flutter-sample/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..b8987c07 --- /dev/null +++ b/flutter-sample/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/flutter-sample/example/android/build.gradle b/flutter-sample/example/android/build.gradle new file mode 100644 index 00000000..6d8124c4 --- /dev/null +++ b/flutter-sample/example/android/build.gradle @@ -0,0 +1,36 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + mavenLocal() + google() + jcenter() + maven { url 'https://developer.huawei.com/repo/' } + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.3.14' + classpath 'com.huawei.agconnect:agcp:1.9.1.300' + } +} + +allprojects { + repositories { + mavenLocal() + google() + maven { url 'https://developer.huawei.com/repo/' } + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/flutter-sample/example/android/gradle.properties b/flutter-sample/example/android/gradle.properties new file mode 100644 index 00000000..f4236ea5 --- /dev/null +++ b/flutter-sample/example/android/gradle.properties @@ -0,0 +1,6 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536M + +android.enableR8=true +android.defaults.buildfeatures.buildconfig=true \ No newline at end of file diff --git a/flutter-sample/example/android/gradle/appLibs.versions.toml b/flutter-sample/example/android/gradle/appLibs.versions.toml new file mode 100644 index 00000000..8089db36 --- /dev/null +++ b/flutter-sample/example/android/gradle/appLibs.versions.toml @@ -0,0 +1,17 @@ +[versions] +glide = "4.16.0" + +[libraries] +fcm = { module = "com.google.firebase:firebase-messaging", version = "23.1.2" } +playLocation = { module = "com.google.android.gms:play-services-location", version = "21.0.1" } +kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib", version = "1.7.10" } +hmsPush = { module = "com.huawei.hms:push", version = "6.10.0.300" } +adIdentifier = { module = "com.google.android.gms:play-services-ads-identifier", version = "18.0.1" } +lifecycleOwner = { module = "androidx.lifecycle:lifecycle-process", version = "2.5.1" } +expresso = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" } +androidJUnit = { module = "androidx.test.ext:junit", version = "1.1.5" } +junit = { module = "junit:junit", version = "4.13.2" } + + +glideCore = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glideCompiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } \ No newline at end of file diff --git a/flutter-sample/example/android/gradle/wrapper/gradle-wrapper.properties b/flutter-sample/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..54833d7b --- /dev/null +++ b/flutter-sample/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Oct 23 15:24:46 IST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip \ No newline at end of file diff --git a/flutter-sample/example/android/settings.gradle b/flutter-sample/example/android/settings.gradle new file mode 100644 index 00000000..b7b84cdb --- /dev/null +++ b/flutter-sample/example/android/settings.gradle @@ -0,0 +1,32 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + libs { + create("moengage") { + from("com.moengage:android-dependency-catalog:3.2.1") + } + create("appLibs") { + from(files("../android/gradle/appLibs.versions.toml")) + } + } + } +} \ No newline at end of file diff --git a/flutter-sample/example/ios/Flutter/.last_build_id b/flutter-sample/example/ios/Flutter/.last_build_id new file mode 100644 index 00000000..ddb522ad --- /dev/null +++ b/flutter-sample/example/ios/Flutter/.last_build_id @@ -0,0 +1 @@ +2d3fe686ad810a0d099d9e3a0d437a80 \ No newline at end of file diff --git a/flutter-sample/example/ios/Flutter/AppFrameworkInfo.plist b/flutter-sample/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..3158eba3 --- /dev/null +++ b/flutter-sample/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 10.0 + + diff --git a/flutter-sample/example/ios/Flutter/Debug.xcconfig b/flutter-sample/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..e8efba11 --- /dev/null +++ b/flutter-sample/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/flutter-sample/example/ios/Flutter/Release.xcconfig b/flutter-sample/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..399e9340 --- /dev/null +++ b/flutter-sample/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/flutter-sample/example/ios/NotificationServices/Info.plist b/flutter-sample/example/ios/NotificationServices/Info.plist new file mode 100644 index 00000000..c683d4b8 --- /dev/null +++ b/flutter-sample/example/ios/NotificationServices/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + NotificationServices + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/flutter-sample/example/ios/NotificationServices/NotificationService.swift b/flutter-sample/example/ios/NotificationServices/NotificationService.swift new file mode 100644 index 00000000..f075fb7e --- /dev/null +++ b/flutter-sample/example/ios/NotificationServices/NotificationService.swift @@ -0,0 +1,32 @@ +// +// NotificationService.swift +// NotificationServices +// +// Created by Chengappa C D on 06/11/20. +// Copyright © 2020 The Chromium Authors. All rights reserved. +// + +import UserNotifications +import MoEngageRichNotification + +class NotificationService: UNNotificationServiceExtension { + + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + MoEngageSDKRichNotification.setAppGroupID("group.com.alphadevs.MoEngage.NotificationServices") + MoEngageSDKRichNotification.handle(richNotificationRequest: request, withContentHandler: contentHandler) + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } + +} diff --git a/flutter-sample/example/ios/NotificationServices/NotificationServices.entitlements b/flutter-sample/example/ios/NotificationServices/NotificationServices.entitlements new file mode 100644 index 00000000..f87b3ea1 --- /dev/null +++ b/flutter-sample/example/ios/NotificationServices/NotificationServices.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.com.alphadevs.MoEngage.NotificationServices + + + diff --git a/flutter-sample/example/ios/Podfile b/flutter-sample/example/ios/Podfile new file mode 100644 index 00000000..37d94cd2 --- /dev/null +++ b/flutter-sample/example/ios/Podfile @@ -0,0 +1,53 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end + +target 'PushTemplates' do + use_frameworks! + pod 'MoEngageRichNotification' + +end + +target 'NotificationServices' do + use_frameworks! + pod 'MoEngageRichNotification' + +end diff --git a/flutter-sample/example/ios/Podfile.lock b/flutter-sample/example/ios/Podfile.lock new file mode 100644 index 00000000..55616be2 --- /dev/null +++ b/flutter-sample/example/ios/Podfile.lock @@ -0,0 +1,104 @@ +PODS: + - Flutter (1.0.0) + - MoEngage-iOS-SDK (9.14.0) + - moengage_cards_ios (1.1.0): + - Flutter + - MoEngagePluginCards (~> 1.2.1) + - moengage_flutter_ios (1.1.0): + - Flutter + - MoEngagePluginBase (~> 4.5.1) + - moengage_geofence_ios (1.1.0): + - Flutter + - MoEngagePluginGeofence (~> 2.5.1) + - moengage_inbox_ios (1.1.0): + - Flutter + - MoEngagePluginInbox (~> 2.5.1) + - MoEngageCards (4.13.0): + - MoEngage-iOS-SDK (< 9.15.0, >= 9.14.0) + - MoEngageGeofence (5.13.0): + - MoEngage-iOS-SDK (< 9.15.0, >= 9.14.0) + - MoEngageInApp (4.13.0): + - MoEngage-iOS-SDK (< 9.15.0, >= 9.14.0) + - MoEngageInbox (2.13.0): + - MoEngage-iOS-SDK (< 9.15.0, >= 9.14.0) + - MoEngageRichNotification (< 7.14.0, >= 7.13.0) + - MoEngagePluginBase (4.5.1): + - MoEngage-iOS-SDK (< 9.15.0, >= 9.14.0) + - MoEngageInApp (< 4.14.0, >= 4.13.0) + - MoEngagePluginCards (1.2.1): + - MoEngageCards (< 4.14.0, >= 4.13.0) + - MoEngagePluginBase (< 4.6.0, >= 4.5.1) + - MoEngagePluginGeofence (2.5.1): + - MoEngageGeofence (< 5.14.0, >= 5.13.0) + - MoEngagePluginBase (< 4.6.0, >= 4.5.1) + - MoEngagePluginInbox (2.5.1): + - MoEngageInbox (< 2.14.0, >= 2.13.0) + - MoEngagePluginBase (< 4.6.0, >= 4.5.1) + - MoEngageRichNotification (7.13.0): + - MoEngage-iOS-SDK (< 9.15.0, >= 9.14.0) + - permission_handler_apple (9.1.1): + - Flutter + - url_launcher (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - moengage_cards_ios (from `.symlinks/plugins/moengage_cards_ios/ios`) + - moengage_flutter_ios (from `.symlinks/plugins/moengage_flutter_ios/ios`) + - moengage_geofence_ios (from `.symlinks/plugins/moengage_geofence_ios/ios`) + - moengage_inbox_ios (from `.symlinks/plugins/moengage_inbox_ios/ios`) + - MoEngageRichNotification + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + +SPEC REPOS: + trunk: + - MoEngage-iOS-SDK + - MoEngageCards + - MoEngageGeofence + - MoEngageInApp + - MoEngageInbox + - MoEngagePluginBase + - MoEngagePluginCards + - MoEngagePluginGeofence + - MoEngagePluginInbox + - MoEngageRichNotification + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + moengage_cards_ios: + :path: ".symlinks/plugins/moengage_cards_ios/ios" + moengage_flutter_ios: + :path: ".symlinks/plugins/moengage_flutter_ios/ios" + moengage_geofence_ios: + :path: ".symlinks/plugins/moengage_geofence_ios/ios" + moengage_inbox_ios: + :path: ".symlinks/plugins/moengage_inbox_ios/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + url_launcher: + :path: ".symlinks/plugins/url_launcher/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + MoEngage-iOS-SDK: fd9507de4c0960ae08874b16e2dfbb27e61e7703 + moengage_cards_ios: c8779cff7f8471bbdba0d9e0be547f5b33ac6c41 + moengage_flutter_ios: a06fc2847800e196d04f904a906d4517d311c012 + moengage_geofence_ios: 0ca6c34c0f6a8dd9ab18130213ff59a473eca0a7 + moengage_inbox_ios: 9702e5d1ad38683a3a476a15c2d91ba6ad35eb44 + MoEngageCards: cee2c1036c0717df737f17b196d2ab42a8ec0cf4 + MoEngageGeofence: 3edba5ea3a366798e3c97777cbf70a7121272c8f + MoEngageInApp: 351f9f107d675bb62a7bf90cc22456b2b41ba606 + MoEngageInbox: 718466210685b88db07594f1d3c5b63106a3e915 + MoEngagePluginBase: 6e2b9153015ce8bd1e45403225fc4c82a04ce6b0 + MoEngagePluginCards: 15f137ff190da9708255476ec0f612a2a91e7f1d + MoEngagePluginGeofence: 128fef574357a422b659a35b0a1dfd3477a551a7 + MoEngagePluginInbox: bc8fbd05ec46b1f76747880eb03bc0adebf4fe2e + MoEngageRichNotification: 2da207f26a952012f3c9d8e86324bfa320b1e96d + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + +PODFILE CHECKSUM: 9a759cd70a24bd7515805f773e51e363bc6453dd + +COCOAPODS: 1.12.1 diff --git a/flutter-sample/example/ios/PushTemplates/Base.lproj/MainInterface.storyboard b/flutter-sample/example/ios/PushTemplates/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..384eb843 --- /dev/null +++ b/flutter-sample/example/ios/PushTemplates/Base.lproj/MainInterface.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter-sample/example/ios/PushTemplates/Info.plist b/flutter-sample/example/ios/PushTemplates/Info.plist new file mode 100644 index 00000000..75b0d1cd --- /dev/null +++ b/flutter-sample/example/ios/PushTemplates/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + PushTemplates + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + UNNotificationExtensionCategory + myNotificationCategory + UNNotificationExtensionInitialContentSizeRatio + 1 + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.usernotifications.content-extension + + + diff --git a/flutter-sample/example/ios/PushTemplates/NotificationViewController.swift b/flutter-sample/example/ios/PushTemplates/NotificationViewController.swift new file mode 100644 index 00000000..b9a69c4d --- /dev/null +++ b/flutter-sample/example/ios/PushTemplates/NotificationViewController.swift @@ -0,0 +1,27 @@ +// +// NotificationViewController.swift +// PushTemplates +// +// Created by Chengappa C D on 23/02/21. +// Copyright © 2021 The Chromium Authors. All rights reserved. +// + +import UIKit +import UserNotifications +import UserNotificationsUI +import MoEngageRichNotification + +class NotificationViewController: UIViewController, UNNotificationContentExtension { + + @IBOutlet var label: UILabel? + + override func viewDidLoad() { + super.viewDidLoad() + MoEngageSDKRichNotification.setAppGroupID("group.com.alphadevs.MoEngage.NotificationServices") + } + + func didReceive(_ notification: UNNotification) { + MoEngageSDKRichNotification.addPushTemplate(toController: self, withNotification: notification) + } + +} diff --git a/flutter-sample/example/ios/PushTemplates/PushTemplates.entitlements b/flutter-sample/example/ios/PushTemplates/PushTemplates.entitlements new file mode 100644 index 00000000..44a37b9b --- /dev/null +++ b/flutter-sample/example/ios/PushTemplates/PushTemplates.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.alphadevs.MoEngage.NotificationServices + + + diff --git a/flutter-sample/example/ios/Runner.xcodeproj/project.pbxproj b/flutter-sample/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f84bb9d4 --- /dev/null +++ b/flutter-sample/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,1107 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 22426FA57D65D497C1DFED9D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EEAC076CF1854AAD1567E3B /* Pods_Runner.framework */; }; + 38790426FEF7EE3A8905CA91 /* Pods_NotificationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3184DD12912BF836B035BE68 /* Pods_NotificationServices.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9A046098FAB1C65F9F02E415 /* Pods_PushTemplates.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B8DE2852222017F358FC39C0 /* Pods_PushTemplates.framework */; }; + AFA7AD352555325C00876691 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA7AD342555325C00876691 /* NotificationService.swift */; }; + AFA7AD392555325C00876691 /* NotificationServices.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = AFA7AD322555325B00876691 /* NotificationServices.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + AFAA33BF25E4CA1D00ABC6F1 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFAA33BE25E4CA1D00ABC6F1 /* UserNotifications.framework */; }; + AFAA33C125E4CA1D00ABC6F1 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFAA33C025E4CA1D00ABC6F1 /* UserNotificationsUI.framework */; }; + AFAA33C425E4CA1D00ABC6F1 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFAA33C325E4CA1D00ABC6F1 /* NotificationViewController.swift */; }; + AFAA33C725E4CA1D00ABC6F1 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AFAA33C525E4CA1D00ABC6F1 /* MainInterface.storyboard */; }; + AFAA33CB25E4CA1D00ABC6F1 /* PushTemplates.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = AFAA33BD25E4CA1D00ABC6F1 /* PushTemplates.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + AFAE14A62639F48400F1069D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AFAE14A52639F48400F1069D /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AFA7AD372555325C00876691 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = AFA7AD312555325B00876691; + remoteInfo = NotificationServices; + }; + AFAA33C925E4CA1D00ABC6F1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = AFAA33BC25E4CA1D00ABC6F1; + remoteInfo = PushTemplates; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + AFA7AD3A2555325C00876691 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + AFAA33CB25E4CA1D00ABC6F1 /* PushTemplates.appex in Embed App Extensions */, + AFA7AD392555325C00876691 /* NotificationServices.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0CE26607B63B0A532F4350D6 /* Pods-NotificationServices.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServices.debug.xcconfig"; path = "Target Support Files/Pods-NotificationServices/Pods-NotificationServices.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3184DD12912BF836B035BE68 /* Pods_NotificationServices.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationServices.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4EEAC076CF1854AAD1567E3B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 67F98076C214F2469D3BBAB0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7119944EC85515A70A7C8B04 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 81141CFEFDEA4ACDD58B30F4 /* Pods-NotificationServices.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServices.release.xcconfig"; path = "Target Support Files/Pods-NotificationServices/Pods-NotificationServices.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AEA39D5052DB04BA01312A25 /* Pods-NotificationServices.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServices.profile.xcconfig"; path = "Target Support Files/Pods-NotificationServices/Pods-NotificationServices.profile.xcconfig"; sourceTree = ""; }; + AF372E0D23A40F9B0091404B /* Runner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + AFA7AD322555325B00876691 /* NotificationServices.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServices.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + AFA7AD342555325C00876691 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + AFA7AD362555325C00876691 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AFA7AD5D2555343500876691 /* NotificationServices.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServices.entitlements; sourceTree = ""; }; + AFAA33BD25E4CA1D00ABC6F1 /* PushTemplates.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PushTemplates.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + AFAA33BE25E4CA1D00ABC6F1 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + AFAA33C025E4CA1D00ABC6F1 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; + AFAA33C325E4CA1D00ABC6F1 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; + AFAA33C625E4CA1D00ABC6F1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + AFAA33C825E4CA1D00ABC6F1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AFAA347025E65DF200ABC6F1 /* PushTemplates.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PushTemplates.entitlements; sourceTree = ""; }; + AFAE14A52639F48400F1069D /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + B8DE2852222017F358FC39C0 /* Pods_PushTemplates.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PushTemplates.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CB5EA0C3B3B0EEE9A6056D33 /* Pods-PushTemplates.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PushTemplates.profile.xcconfig"; path = "Target Support Files/Pods-PushTemplates/Pods-PushTemplates.profile.xcconfig"; sourceTree = ""; }; + D09C1AF846000DD010A9DAD1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + EFD79CDFF3A9C8836CB1525D /* Pods-PushTemplates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PushTemplates.debug.xcconfig"; path = "Target Support Files/Pods-PushTemplates/Pods-PushTemplates.debug.xcconfig"; sourceTree = ""; }; + FA447C77210C2C5B421B7F32 /* Pods-PushTemplates.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PushTemplates.release.xcconfig"; path = "Target Support Files/Pods-PushTemplates/Pods-PushTemplates.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 22426FA57D65D497C1DFED9D /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AFA7AD2F2555325B00876691 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 38790426FEF7EE3A8905CA91 /* Pods_NotificationServices.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AFAA33BA25E4CA1D00ABC6F1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AFAA33C125E4CA1D00ABC6F1 /* UserNotificationsUI.framework in Frameworks */, + AFAA33BF25E4CA1D00ABC6F1 /* UserNotifications.framework in Frameworks */, + 9A046098FAB1C65F9F02E415 /* Pods_PushTemplates.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 46155005AEE9F63CAF5F44AC /* Frameworks */ = { + isa = PBXGroup; + children = ( + AFAA33BE25E4CA1D00ABC6F1 /* UserNotifications.framework */, + AFAA33C025E4CA1D00ABC6F1 /* UserNotificationsUI.framework */, + 4EEAC076CF1854AAD1567E3B /* Pods_Runner.framework */, + 3184DD12912BF836B035BE68 /* Pods_NotificationServices.framework */, + B8DE2852222017F358FC39C0 /* Pods_PushTemplates.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5E275FE1A6ACAE765C33FB19 /* Pods */ = { + isa = PBXGroup; + children = ( + 67F98076C214F2469D3BBAB0 /* Pods-Runner.debug.xcconfig */, + 7119944EC85515A70A7C8B04 /* Pods-Runner.release.xcconfig */, + D09C1AF846000DD010A9DAD1 /* Pods-Runner.profile.xcconfig */, + 0CE26607B63B0A532F4350D6 /* Pods-NotificationServices.debug.xcconfig */, + 81141CFEFDEA4ACDD58B30F4 /* Pods-NotificationServices.release.xcconfig */, + AEA39D5052DB04BA01312A25 /* Pods-NotificationServices.profile.xcconfig */, + EFD79CDFF3A9C8836CB1525D /* Pods-PushTemplates.debug.xcconfig */, + FA447C77210C2C5B421B7F32 /* Pods-PushTemplates.release.xcconfig */, + CB5EA0C3B3B0EEE9A6056D33 /* Pods-PushTemplates.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + AFA7AD332555325C00876691 /* NotificationServices */, + AFAA33C225E4CA1D00ABC6F1 /* PushTemplates */, + 97C146EF1CF9000F007C117D /* Products */, + 5E275FE1A6ACAE765C33FB19 /* Pods */, + 46155005AEE9F63CAF5F44AC /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + AFA7AD322555325B00876691 /* NotificationServices.appex */, + AFAA33BD25E4CA1D00ABC6F1 /* PushTemplates.appex */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + AFAE14A52639F48400F1069D /* GoogleService-Info.plist */, + AF372E0D23A40F9B0091404B /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; + AFA7AD332555325C00876691 /* NotificationServices */ = { + isa = PBXGroup; + children = ( + AFA7AD5D2555343500876691 /* NotificationServices.entitlements */, + AFA7AD342555325C00876691 /* NotificationService.swift */, + AFA7AD362555325C00876691 /* Info.plist */, + ); + path = NotificationServices; + sourceTree = ""; + }; + AFAA33C225E4CA1D00ABC6F1 /* PushTemplates */ = { + isa = PBXGroup; + children = ( + AFAA347025E65DF200ABC6F1 /* PushTemplates.entitlements */, + AFAA33C325E4CA1D00ABC6F1 /* NotificationViewController.swift */, + AFAA33C525E4CA1D00ABC6F1 /* MainInterface.storyboard */, + AFAA33C825E4CA1D00ABC6F1 /* Info.plist */, + ); + path = PushTemplates; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 704D28E40F40B62040EAF4A2 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + AFA7AD3A2555325C00876691 /* Embed App Extensions */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + DC047FF7745E649342F78A5B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + AFA7AD382555325C00876691 /* PBXTargetDependency */, + AFAA33CA25E4CA1D00ABC6F1 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + AFA7AD312555325B00876691 /* NotificationServices */ = { + isa = PBXNativeTarget; + buildConfigurationList = AFA7AD3E2555325C00876691 /* Build configuration list for PBXNativeTarget "NotificationServices" */; + buildPhases = ( + 6DE582793A9498C46122BA62 /* [CP] Check Pods Manifest.lock */, + AFA7AD2E2555325B00876691 /* Sources */, + AFA7AD2F2555325B00876691 /* Frameworks */, + AFA7AD302555325B00876691 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NotificationServices; + productName = NotificationServices; + productReference = AFA7AD322555325B00876691 /* NotificationServices.appex */; + productType = "com.apple.product-type.app-extension"; + }; + AFAA33BC25E4CA1D00ABC6F1 /* PushTemplates */ = { + isa = PBXNativeTarget; + buildConfigurationList = AFAA33CF25E4CA1D00ABC6F1 /* Build configuration list for PBXNativeTarget "PushTemplates" */; + buildPhases = ( + 23353D2BEF2A83CA40AC60D8 /* [CP] Check Pods Manifest.lock */, + AFAA33B925E4CA1D00ABC6F1 /* Sources */, + AFAA33BA25E4CA1D00ABC6F1 /* Frameworks */, + AFAA33BB25E4CA1D00ABC6F1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PushTemplates; + productName = PushTemplates; + productReference = AFAA33BD25E4CA1D00ABC6F1 /* PushTemplates.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = BPWYRAF5X7; + LastSwiftMigration = 1200; + }; + AFA7AD312555325B00876691 = { + CreatedOnToolsVersion = 12.0; + DevelopmentTeam = BPWYRAF5X7; + ProvisioningStyle = Automatic; + }; + AFAA33BC25E4CA1D00ABC6F1 = { + CreatedOnToolsVersion = 12.4; + DevelopmentTeam = BPWYRAF5X7; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + AFA7AD312555325B00876691 /* NotificationServices */, + AFAA33BC25E4CA1D00ABC6F1 /* PushTemplates */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + AFAE14A62639F48400F1069D /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AFA7AD302555325B00876691 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AFAA33BB25E4CA1D00ABC6F1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AFAA33C725E4CA1D00ABC6F1 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 23353D2BEF2A83CA40AC60D8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-PushTemplates-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 6DE582793A9498C46122BA62 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NotificationServices-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 704D28E40F40B62040EAF4A2 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + DC047FF7745E649342F78A5B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/MoEngagePluginBase/MoEngagePluginBase.framework", + "${BUILT_PRODUCTS_DIR}/MoEngagePluginCards/MoEngagePluginCards.framework", + "${BUILT_PRODUCTS_DIR}/MoEngagePluginGeofence/MoEngagePluginGeofence.framework", + "${BUILT_PRODUCTS_DIR}/MoEngagePluginInbox/MoEngagePluginInbox.framework", + "${BUILT_PRODUCTS_DIR}/moengage_cards_ios/moengage_cards_ios.framework", + "${BUILT_PRODUCTS_DIR}/moengage_flutter_ios/moengage_flutter_ios.framework", + "${BUILT_PRODUCTS_DIR}/moengage_geofence_ios/moengage_geofence_ios.framework", + "${BUILT_PRODUCTS_DIR}/moengage_inbox_ios/moengage_inbox_ios.framework", + "${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngage-iOS-SDK/MoEngageSDK.framework/MoEngageSDK", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngage-iOS-SDK/MoEngageCore.framework/MoEngageCore", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngage-iOS-SDK/MoEngageAnalytics.framework/MoEngageAnalytics", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngage-iOS-SDK/MoEngageMessaging.framework/MoEngageMessaging", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngage-iOS-SDK/MoEngageObjCUtils.framework/MoEngageObjCUtils", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngage-iOS-SDK/MoEngageSecurity.framework/MoEngageSecurity", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngageCards/MoEngageCards.framework/MoEngageCards", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngageGeofence/MoEngageGeofence.framework/MoEngageGeofence", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngageInApp/MoEngageInApps.framework/MoEngageInApps", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngageInbox/MoEngageInbox.framework/MoEngageInbox", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/MoEngageRichNotification/MoEngageRichNotification.framework/MoEngageRichNotification", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngagePluginBase.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngagePluginCards.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngagePluginGeofence.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngagePluginInbox.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/moengage_cards_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/moengage_flutter_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/moengage_geofence_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/moengage_inbox_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageSDK.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageCore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageAnalytics.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageMessaging.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageObjCUtils.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageSecurity.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageCards.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageGeofence.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageInApps.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageInbox.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoEngageRichNotification.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AFA7AD2E2555325B00876691 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AFA7AD352555325C00876691 /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AFAA33B925E4CA1D00ABC6F1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AFAA33C425E4CA1D00ABC6F1 /* NotificationViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AFA7AD382555325C00876691 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AFA7AD312555325B00876691 /* NotificationServices */; + targetProxy = AFA7AD372555325C00876691 /* PBXContainerItemProxy */; + }; + AFAA33CA25E4CA1D00ABC6F1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AFAA33BC25E4CA1D00ABC6F1 /* PushTemplates */; + targetProxy = AFAA33C925E4CA1D00ABC6F1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + AFAA33C525E4CA1D00ABC6F1 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AFAA33C625E4CA1D00ABC6F1 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = 1.0.0; + DEVELOPMENT_TEAM = BPWYRAF5X7; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = 1.0.0; + DEVELOPMENT_TEAM = BPWYRAF5X7; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = 1.0.0; + DEVELOPMENT_TEAM = BPWYRAF5X7; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + AFA7AD3B2555325C00876691 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0CE26607B63B0A532F4350D6 /* Pods-NotificationServices.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationServices/NotificationServices.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BPWYRAF5X7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = NotificationServices/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage.NotificationServices; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AFA7AD3C2555325C00876691 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 81141CFEFDEA4ACDD58B30F4 /* Pods-NotificationServices.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationServices/NotificationServices.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BPWYRAF5X7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = NotificationServices/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage.NotificationServices; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AFA7AD3D2555325C00876691 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AEA39D5052DB04BA01312A25 /* Pods-NotificationServices.profile.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationServices/NotificationServices.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BPWYRAF5X7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = NotificationServices/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage.NotificationServices; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; + AFAA33CC25E4CA1D00ABC6F1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EFD79CDFF3A9C8836CB1525D /* Pods-PushTemplates.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = PushTemplates/PushTemplates.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BPWYRAF5X7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = PushTemplates/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage.PushTemplates; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AFAA33CD25E4CA1D00ABC6F1 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FA447C77210C2C5B421B7F32 /* Pods-PushTemplates.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = PushTemplates/PushTemplates.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BPWYRAF5X7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = PushTemplates/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage.PushTemplates; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AFAA33CE25E4CA1D00ABC6F1 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CB5EA0C3B3B0EEE9A6056D33 /* Pods-PushTemplates.profile.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = PushTemplates/PushTemplates.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BPWYRAF5X7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = PushTemplates/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.alphadevs.MoEngage.PushTemplates; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AFA7AD3E2555325C00876691 /* Build configuration list for PBXNativeTarget "NotificationServices" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AFA7AD3B2555325C00876691 /* Debug */, + AFA7AD3C2555325C00876691 /* Release */, + AFA7AD3D2555325C00876691 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AFAA33CF25E4CA1D00ABC6F1 /* Build configuration list for PBXNativeTarget "PushTemplates" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AFAA33CC25E4CA1D00ABC6F1 /* Debug */, + AFAA33CD25E4CA1D00ABC6F1 /* Release */, + AFAA33CE25E4CA1D00ABC6F1 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/flutter-sample/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/flutter-sample/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/flutter-sample/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/flutter-sample/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter-sample/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..76842b7e --- /dev/null +++ b/flutter-sample/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter-sample/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/flutter-sample/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/flutter-sample/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/flutter-sample/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter-sample/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/flutter-sample/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter-sample/example/ios/Runner/AppDelegate.swift b/flutter-sample/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..5dc7b4b5 --- /dev/null +++ b/flutter-sample/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,64 @@ +import UIKit +import Flutter +import moengage_flutter_ios +import MoEngageSDK + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + var flutterViewController: FlutterViewController? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + let yourAppID = "DAO6UGZ73D9RTK8B5W96TPYN" //App ID: You can be obtain it from App Settings in MoEngage Dashboard. + let sdkConfig = MoEngageSDKConfig(withAppID: yourAppID) + sdkConfig.appGroupID = "group.com.alphadevs.MoEngage.NotificationServices" + sdkConfig.enableLogs = true + + MoEngageInitializer.sharedInstance.initializeDefaultInstance(sdkConfig, launchOptions: launchOptions) + + + flutterViewController = FlutterViewController() + let nav = UINavigationController.init(rootViewController: flutterViewController!) + nav.isNavigationBarHidden = true + + let window = UIWindow() + self.window = window + window.rootViewController = nav + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + if let vc = UIApplication.shared.keyWindow?.rootViewController as? FlutterViewController { + return + } + } + + override func registrar(forPlugin: String) -> FlutterPluginRegistrar { + return (self.flutterViewController?.registrar(forPlugin: forPlugin))! + } + + override func hasPlugin(_ pluginKey: String) -> Bool { + return (self.flutterViewController?.hasPlugin(pluginKey))! + } + + override func valuePublished(byPlugin pluginKey: String) -> NSObject { + return (self.flutterViewController?.valuePublished(byPlugin: pluginKey))! + } + + override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + print("Opening deeplink", url) + return true + } + + override func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool { + print("Opening Universal link", userActivityType) + return false + } +} diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..28c6bf03 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f091b6b0 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cde1211 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..d0ef06e7 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..dcdc2306 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..c8f9ed8f Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..75b2d164 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..c4df70d3 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..6a84f41e Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..d0e1f585 Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/flutter-sample/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/flutter-sample/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/flutter-sample/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/flutter-sample/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter-sample/example/ios/Runner/Base.lproj/Main.storyboard b/flutter-sample/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..19645af3 --- /dev/null +++ b/flutter-sample/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/flutter-sample/example/ios/Runner/GoogleService-Info.plist b/flutter-sample/example/ios/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..7f745ea4 --- /dev/null +++ b/flutter-sample/example/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,40 @@ + + + + + AD_UNIT_ID_FOR_BANNER_TEST + ca-app-pub-3940256099942544/2934735716 + AD_UNIT_ID_FOR_INTERSTITIAL_TEST + ca-app-pub-3940256099942544/4411468910 + CLIENT_ID + 559753655227-mqfi4o4fejqi0cfhhglgg6ngu73fd9j5.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.559753655227-mqfi4o4fejqi0cfhhglgg6ngu73fd9j5 + API_KEY + AIzaSyBBbhBuR40Vd6JKZb5b3aUBjVCdP4W9Ns8 + GCM_SENDER_ID + 559753655227 + PLIST_VERSION + 1 + BUNDLE_ID + com.alphadevs.MoEngage + PROJECT_ID + moedemo-93e2e + STORAGE_BUCKET + moedemo-93e2e.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:559753655227:ios:522230ce3e404cf6 + DATABASE_URL + https://moedemo-93e2e.firebaseio.com + + diff --git a/flutter-sample/example/ios/Runner/Info.plist b/flutter-sample/example/ios/Runner/Info.plist new file mode 100644 index 00000000..31bf1527 --- /dev/null +++ b/flutter-sample/example/ios/Runner/Info.plist @@ -0,0 +1,69 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + moengage_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + moeapp + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSLocationAlwaysAndWhenInUseUsageDescription + Need Location permission to monitor Geofence + NSLocationWhenInUseUsageDescription + Need Location permission to monitor Geofence + UIBackgroundModes + + fetch + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/flutter-sample/example/ios/Runner/Runner-Bridging-Header.h b/flutter-sample/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..7335fdf9 --- /dev/null +++ b/flutter-sample/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/flutter-sample/example/ios/Runner/Runner.entitlements b/flutter-sample/example/ios/Runner/Runner.entitlements new file mode 100644 index 00000000..f87b3ea1 --- /dev/null +++ b/flutter-sample/example/ios/Runner/Runner.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.com.alphadevs.MoEngage.NotificationServices + + + diff --git a/flutter-sample/example/lib/cards/card_widget.dart b/flutter-sample/example/lib/cards/card_widget.dart new file mode 100644 index 00000000..9302e362 --- /dev/null +++ b/flutter-sample/example/lib/cards/card_widget.dart @@ -0,0 +1,431 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:moengage_cards/moengage_cards.dart' as moe; +import 'cards_helper.dart'; +import 'utils.dart'; + +enum CardActionEvent { CLICK, DELETE, SHOWN } + +typedef CardActionCallback = void + Function(CardActionEvent cardActionEvent, moe.Card card, {int widgetId}); + +class IllustrationCard extends StatefulWidget { + const IllustrationCard(this.card, this.callback, {super.key}); + final CardActionCallback callback; + final moe.Card card; + + @override + State createState() => _IllustrationCardState(); +} + +class _IllustrationCardState extends State { + @override + void initState() { + super.initState(); + widget.callback.call(CardActionEvent.SHOWN, widget.card); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Container( + child: Card( + color: + colorFromHex(widget.card.getContainer()?.style?.backgroundColor), + child: Container( + foregroundDecoration: + (widget.card.metaData.campaignState.isClicked == false) + ? const BadgeDecoration() + : const BoxDecoration(), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12), + child: Column( + children: [ + Visibility( + visible: widget.card.metaData.displayControl.isPinned, + child: Row( + children: const [ + Spacer(), + Icon( + Icons.bookmark_added_rounded, + color: Colors.grey, + ), + ], + ), + ), + getImageWidget(context), + getHeaderText(context), + getMessageText(context), + const Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + getDateFromMillis( + widget.card.metaData.updatedTime * 1000), + style: const TextStyle(color: Colors.grey), + ), + const Spacer(), + getCtaText(context) + ], + ) + ], + ), + ), + ), + )), + onTap: () { + widget.callback.call(CardActionEvent.CLICK, widget.card); + handleWidgetActions(widget.card.getContainer()?.actionList); + }, + onLongPress: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: GestureDetector( + child: const Text( + 'Delete', + style: TextStyle( + fontWeight: FontWeight.normal, + color: Colors.black54, + ), + ), + onTap: () { + Navigator.of(context).pop(); + widget.callback.call(CardActionEvent.DELETE, widget.card); + }, + ), + ); + }, + ); + }); + } + + Widget getImageWidget(BuildContext context) { + moe.Widget? image = widget.card.getImageWidget(); + if (image == null || image.content.isEmpty) return const SizedBox.shrink(); + return Image.network( + image.content, + fit: BoxFit.fill, + height: 150, + width: MediaQuery.of(context).size.width, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ); + } + + Widget getHeaderText(BuildContext context) { + moe.Widget? header = widget.card.getHeaderWidget(); + if (header == null || header.content.isEmpty) { + return const SizedBox.shrink(); + } + return Html( + data: header.content, + style: { + '#': Style( + fontSize: FontSize(16), + maxLines: 1, + textOverflow: TextOverflow.ellipsis, + ), + 'body': Style( + margin: Margins.zero, + ) + }, + ); + } + + Widget getMessageText(BuildContext context) { + moe.Widget? message = widget.card.getMessageWidget(); + if (message == null || message.content.isEmpty) { + return const SizedBox.shrink(); + } + return Html( + data: message.content, + style: { + '#': Style( + fontSize: FontSize(12), + maxLines: 4, + textOverflow: TextOverflow.ellipsis, + ), + 'body': Style( + margin: Margins.zero, + ) + }, + ); + } + + Widget getCtaText(BuildContext context) { + moe.Widget? cta = widget.card.getButtonWidget(); + if (cta == null || cta.content.isEmpty) return const SizedBox.shrink(); + return TextButton( + onPressed: () { + widget.callback + .call(CardActionEvent.CLICK, widget.card, widgetId: cta.id); + handleWidgetActions(cta.actionList); + }, + child: Html( + data: cta.content, + shrinkWrap: true, + style: { + '#': Style( + fontSize: FontSize(12), + maxLines: 1, + textOverflow: TextOverflow.ellipsis, + ), + 'body': Style( + margin: Margins.zero, + ) + }, + )); + } +} + +class BasicCard extends StatefulWidget { + const BasicCard(this.card, this.callback, {super.key}); + final CardActionCallback callback; + final moe.Card card; + + @override + State createState() => _BasicCardState(); +} + +class _BasicCardState extends State { + @override + void initState() { + super.initState(); + widget.callback.call(CardActionEvent.SHOWN, widget.card); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Container( + child: Card( + color: + colorFromHex(widget.card.getContainer()?.style?.backgroundColor), + child: Container( + foregroundDecoration: + (widget.card.metaData.campaignState.isClicked == false) + ? const BadgeDecoration() + : const BoxDecoration(), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12), + child: Column( + children: [ + Visibility( + visible: widget.card.metaData.displayControl.isPinned, + child: Row( + children: const [ + Spacer(), + Icon( + Icons.bookmark_added_rounded, + color: Colors.grey, + ), + ], + ), + ), + Row( + children: [ + getImageWidget(context), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + children: [ + getHeaderText(context), + getMessageText(context), + ], + ), + ), + ), + ], + ), + const Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + getDateFromMillis( + widget.card.metaData.updatedTime * 1000), + style: const TextStyle(color: Colors.grey), + ), + const Spacer(), + getCtaText(context) + ], + ) + ], + ), + ), + ), + )), + onTap: () { + widget.callback.call(CardActionEvent.CLICK, widget.card); + handleWidgetActions(widget.card.getContainer()?.actionList); + }, + onLongPress: () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: GestureDetector( + child: const Text( + 'Delete', + style: TextStyle( + fontWeight: FontWeight.normal, + color: Colors.black54, + ), + ), + onTap: () { + Navigator.of(context).pop(); + widget.callback.call(CardActionEvent.DELETE, widget.card); + }, + ), + ); + }, + ); + }); + } + + Widget getImageWidget(BuildContext context) { + moe.Widget? image = widget.card.getImageWidget(); + if (image == null || image.content.isEmpty) return const SizedBox.shrink(); + return Image.network( + image.content, + fit: BoxFit.fill, + width: MediaQuery.of(context).size.width * .2, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ); + } + + Widget getHeaderText(BuildContext context) { + moe.Widget? header = widget.card.getHeaderWidget(); + if (header == null || header.content.isEmpty) { + return const SizedBox.shrink(); + } + return Html( + data: header.content, + style: { + '#': Style( + fontSize: FontSize(16), + maxLines: 1, + textOverflow: TextOverflow.ellipsis, + ), + 'body': Style( + margin: Margins.zero, + ) + }, + ); + } + + Widget getMessageText(BuildContext context) { + moe.Widget? message = widget.card.getMessageWidget(); + if (message == null || message.content.isEmpty) { + return const SizedBox.shrink(); + } + return Html( + data: message.content, + style: { + '#': Style( + fontSize: FontSize(12), + maxLines: 4, + textOverflow: TextOverflow.ellipsis, + ), + 'body': Style( + margin: Margins.zero, + ) + }, + ); + } + + Widget getCtaText(BuildContext context) { + moe.Widget? cta = widget.card.getButtonWidget(); + if (cta == null || cta.content.isEmpty) return const SizedBox.shrink(); + return TextButton( + onPressed: () { + widget.callback + .call(CardActionEvent.CLICK, widget.card, widgetId: cta.id); + handleWidgetActions(cta.actionList); + }, + child: Html( + data: cta.content, + shrinkWrap: true, + style: { + '#': Style( + fontSize: FontSize(16), + maxLines: 1, + textOverflow: TextOverflow.ellipsis, + ), + 'body': Style( + margin: Margins.zero, + ) + }, + )); + } +} + +class BadgeDecoration extends Decoration { + const BadgeDecoration( + {this.badgeColor = Colors.lightBlue, this.badgeSize = 15}); + final Color badgeColor; + final double badgeSize; + + @override + BoxPainter createBoxPainter([VoidCallback? callback]) => + _BadgePainter(badgeColor, badgeSize); +} + +class _BadgePainter extends BoxPainter { + _BadgePainter(this.badgeColor, this.badgeSize); + static const double CORNER_RADIUS = 4; + final Color badgeColor; + final double badgeSize; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + canvas.save(); + canvas.translate( + offset.dx + (configuration.size?.width ?? 0) - badgeSize, offset.dy); + canvas.drawPath(buildBadgePath(), getBadgePaint()); + canvas.restore(); + } + + Paint getBadgePaint() => Paint() + ..isAntiAlias = true + ..color = badgeColor; + + Path buildBadgePath() => Path.combine( + PathOperation.difference, + Path() + ..addRRect(RRect.fromLTRBAndCorners(0, 0, badgeSize, badgeSize, + topRight: const Radius.circular(CORNER_RADIUS))), + Path() + ..lineTo(0, badgeSize) + ..lineTo(badgeSize, badgeSize) + ..close()); +} diff --git a/flutter-sample/example/lib/cards/cards_helper.dart b/flutter-sample/example/lib/cards/cards_helper.dart new file mode 100644 index 00000000..d41e10f2 --- /dev/null +++ b/flutter-sample/example/lib/cards/cards_helper.dart @@ -0,0 +1,30 @@ +// ignore_for_file: public_member_api_docs + +import 'package:collection/collection.dart'; +import 'package:moengage_cards/moengage_cards.dart'; + +extension CardExtension on Card { + Widget? getImageWidget() { + return template.containers[0].widgets + .firstWhereOrNull((Widget w) => w.widgetType == WidgetType.image); + } + + Widget? getHeaderWidget() { + return template.containers[0].widgets.firstWhereOrNull( + (Widget w) => w.widgetType == WidgetType.text && w.id == 1); + } + + Widget? getMessageWidget() { + return template.containers[0].widgets.firstWhereOrNull( + (Widget w) => w.widgetType == WidgetType.text && w.id == 2); + } + + Widget? getButtonWidget() { + return template.containers[0].widgets + .firstWhereOrNull((Widget w) => w.widgetType == WidgetType.button); + } + + Container? getContainer() { + return template.containers.firstOrNull; + } +} diff --git a/flutter-sample/example/lib/cards/cards_home.dart b/flutter-sample/example/lib/cards/cards_home.dart new file mode 100644 index 00000000..758895ef --- /dev/null +++ b/flutter-sample/example/lib/cards/cards_home.dart @@ -0,0 +1,113 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:moengage_cards/moengage_cards.dart' as moe; + +import 'cards_screen.dart'; + +class CardsHome extends StatefulWidget { + const CardsHome({super.key}); + + @override + State createState() => _CardsHomeState(); +} + +class _CardsHomeState extends State { + moe.MoEngageCards cards = moe.MoEngageCards('DAO6UGZ73D9RTK8B5W96TPYN'); + + @override + void initState() { + super.initState(); + cards.setAppOpenCardsSyncListener((moe.SyncCompleteData? data) { + debugPrint('Cards App Open Sync Listener: $data'); + }); + cards.initialize(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Cards Home'), + ), + body: Column( + children: [ + ListView( + shrinkWrap: true, + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTile( + title: const Text('New Cards Count'), + onTap: () { + cards.getNewCardsCount().then((int value) { + showSnackBar('New Cards Count: $value', context); + }); + }, + ), + ListTile( + title: const Text('UnClicked Cards Count'), + onTap: () { + cards.getUnClickedCardsCount().then((int value) { + showSnackBar('UnClicked Cards Count: $value', context); + }); + }, + ), + ListTile( + title: const Text('Get Cards Categories'), + onTap: () { + cards.getCardsCategories().then((List value) { + showSnackBar('Get Cards Categories: $value', context); + }); + }, + ), + ListTile( + title: const Text('Is All Category Enabled'), + onTap: () { + cards.isAllCategoryEnabled().then((bool value) { + showSnackBar( + 'Is All Category Enabled: $value', context); + }); + }, + ), + ListTile( + title: const Text('Mark Card as Delivered'), + onTap: () async { + cards.cardDelivered(); + showSnackBar('Marking Card as Delivered', context); + }, + ), + ListTile( + title: const Text('Fetch Cards'), + onTap: () async { + final moe.CardsData data = await cards.fetchCards(); + final int count = data.cards.length; + // ignore: use_build_context_synchronously + showSnackBar( + 'Fetched $count card(s) , Category-${data.category}', + context); + }, + ), + ListTile( + title: const Text('Go To Cards UI'), + tileColor: Colors.blueGrey.shade50.withAlpha(100), + onTap: () async { + await Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => + const CardsScreen())); + }, + ), + ], + ).toList(), + ), + ], + )); + } + + showSnackBar(String text, BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(text), + duration: const Duration(seconds: 2), + )); + } +} diff --git a/flutter-sample/example/lib/cards/cards_screen.dart b/flutter-sample/example/lib/cards/cards_screen.dart new file mode 100644 index 00000000..36575d9e --- /dev/null +++ b/flutter-sample/example/lib/cards/cards_screen.dart @@ -0,0 +1,261 @@ +// ignore_for_file: public_member_api_docs +import 'package:flutter/material.dart'; +import 'package:moengage_cards/moengage_cards.dart' as moe; +import 'card_widget.dart'; + +class CardsScreen extends StatefulWidget { + const CardsScreen({super.key}); + + @override + State createState() => _CardsScreenState(); +} + +class _CardsScreenState extends State + with TickerProviderStateMixin { + List cardList = []; + + moe.MoEngageCards cards = moe.MoEngageCards('DAO6UGZ73D9RTK8B5W96TPYN'); + + List categories = []; + + late TabController tabController; + + bool showHasUpdates = false; + + bool showLoader = false; + + @override + void initState() { + super.initState(); + cards.setAppOpenCardsSyncListener((moe.SyncCompleteData? data) { + debugPrint('Cards App Open Sync Listener Callback: $data'); + }); + setUpTabs(); + cards.onCardsSectionLoaded((moe.SyncCompleteData? data) { + debugPrint('onCardsSectionLoaded(): Callback Data: $data'); + if (data?.hasUpdates == true) { + setState(() { + showHasUpdates = true; + }); + } + }); + fetchCards(); + cards.initialize(); + } + + void fetchCards() { + cards.getCardsInfo().then((moe.CardsInfo data) { + setState(() { + cardList = data.cards; + categories.clear(); + if (data.shouldShowAllTab && data.categories.isNotEmpty) { + categories.add('All'); + } + categories.addAll(data.categories); + setUpTabs(); + }); + }); + } + + void setUpTabs() { + tabController = TabController( + length: categories.length, + vsync: this, + ); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + cards.onCardsSectionUnLoaded(); + return true; + }, + child: Scaffold( + appBar: AppBar( + centerTitle: true, + iconTheme: const IconThemeData(color: Colors.black54), + backgroundColor: Colors.white, + shadowColor: Colors.grey.shade100, + title: const Text( + 'Inbox', + style: TextStyle(color: Colors.black54), + ), + ), + body: RefreshIndicator( + onRefresh: () async { + refreshCards(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: MediaQuery.of(context).size.height - + AppBar().preferredSize.height, + child: (categories.isEmpty) + ? const Center( + child: Text('No Data'), + ) + : getListWidget(), + ), + )), + )); + } + + Widget getListWidget() { + return Container( + child: Stack( + children: [ + Stack( + children: [ + Column( + children: [ + TabBar( + isScrollable: true, + padding: const EdgeInsets.all(8.0), + indicator: const UnderlineTabIndicator( + borderSide: BorderSide(width: 3.0, color: Colors.black54), + insets: EdgeInsets.symmetric(horizontal: 16.0), + ), + indicatorSize: TabBarIndicatorSize.label, + tabs: categories + .map((String category) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + category, + style: const TextStyle( + color: Colors.black38, + fontSize: 18, + ), + ), + )) + .toList(), + controller: tabController, + ), + Expanded( + child: TabBarView( + controller: tabController, + children: categories + .map((category) => + CardsListWidget(category, cards, actionCallback)) + .toList(), + )) + ], + ), + if (showHasUpdates) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 24.0), + child: OutlinedButton( + onPressed: () { + fetchCards(); + setState(() { + showHasUpdates = false; + }); + }, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.white), + shape: MaterialStateProperty + .all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + side: const BorderSide(color: Colors.black54))), + side: MaterialStateProperty.all( + const BorderSide(), + ), + ), + child: const Text( + 'New Updates', + style: TextStyle(color: Colors.black54), + ), + ), + ), + ], + ) + else + const SizedBox.shrink() + ], + ) + ], + )); + } + + void refreshCards() { + cards.refreshCards((moe.SyncCompleteData? data) { + debugPrint('refreshCards(): Callback Data: $data'); + cards.cardDelivered(); + if (data?.hasUpdates == true) { + fetchCards(); + } + }); + } + + void cardClicked(moe.Card card) { + cards.cardClicked(card, card.template.containers[0].id); + } + + void actionCallback(CardActionEvent event, moe.Card card, + {int widgetId = -1}) { + if (event == CardActionEvent.CLICK) { + cards.cardClicked(card, widgetId); + } else if (event == CardActionEvent.DELETE) { + cards.deleteCard(card); + setState(() { + cardList.remove(card); + fetchCards(); + }); + } else if (event == CardActionEvent.SHOWN) { + cards.cardShown(card); + } + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } +} + +class CardsListWidget extends StatefulWidget { + const CardsListWidget(this.category, this.cards, this.actionCallback, + {super.key}); + + final String category; + final moe.MoEngageCards cards; + final CardActionCallback actionCallback; + + @override + State createState() => _CardsListWidgetState(); +} + +class _CardsListWidgetState extends State { + @override + Widget build(BuildContext context) { + return Container( + child: FutureBuilder( + future: widget.cards.getCardsForCategory(widget.category), + builder: + (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + final List data = snapshot.data?.cards ?? []; + if (data.isEmpty) { + return const Center(child: Text('No Data')); + } + return ListView.builder( + itemCount: data.length, + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int pos) { + return (data[pos].template.templateType == + moe.TemplateType.basic) + ? BasicCard(data[pos], widget.actionCallback) + : IllustrationCard(data[pos], widget.actionCallback); + }); + } + return const Center(child: CircularProgressIndicator()); + }), + ); + } +} diff --git a/flutter-sample/example/lib/cards/utils.dart b/flutter-sample/example/lib/cards/utils.dart new file mode 100644 index 00000000..dfc0406b --- /dev/null +++ b/flutter-sample/example/lib/cards/utils.dart @@ -0,0 +1,48 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:moengage_cards/moengage_cards.dart' as moe; +import 'package:url_launcher/url_launcher.dart'; + +Color? colorFromHex(String? hexColor) { + if (hexColor == null) { + return null; + } + final String hexCode = hexColor.replaceAll('#', ''); + return Color(int.parse('FF$hexCode', radix: 16)); +} + +String getDateFromMillis(int timeInMillis) { + final DateFormat format = DateFormat('dd MMM,yy hh:mm a'); + return format.format(DateTime.fromMillisecondsSinceEpoch(timeInMillis)); +} + +Future handleAction(moe.Action action) async { + if (action.actionType == moe.ActionType.navigate) { + action = action as moe.NavigationAction; + if (action.navigationType == moe.NavigationType.screenName) { + debugPrint('Screen Name Navigation Not supported'); + return; + } + if (action.value.isEmpty) { + debugPrint('Url Empty'); + return; + } + final Uri uri = + Uri.parse(action.value).replace(queryParameters: action.keyValuePairs); + if (action.navigationType == moe.NavigationType.richLanding) { + //Open RichLanding Url in WebView Inside App + await launch(uri.toString(), forceWebView: true, forceSafariVC: true); + } else if (action.navigationType == moe.NavigationType.deepLink) { + //Open DeepLink In External App + await launch(uri.toString()); + } + } +} + +handleWidgetActions(List? actions) { + actions?.forEach((moe.Action action) { + handleAction(action); + }); +} diff --git a/flutter-sample/example/lib/main.dart b/flutter-sample/example/lib/main.dart new file mode 100644 index 00000000..2c134a37 --- /dev/null +++ b/flutter-sample/example/lib/main.dart @@ -0,0 +1,630 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:moengage_flutter/moengage_flutter.dart'; +import 'package:moengage_geofence/moengage_geofence.dart'; +import 'package:moengage_inbox/moengage_inbox.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'cards/cards_home.dart'; +import 'second_page.dart'; +import 'utils.dart'; +import 'package:flutter/foundation.dart'; +import 'dart:convert'; +import 'dart:core'; +import 'dart:html'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + // FirebaseApp not configured for web app. Added the check to avoid run time errors. + if (!kIsWeb) { + await Firebase.initializeApp(); + // Set the background messaging handler early on, as a named top-level function + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + } + runApp(const MaterialApp(home: MyApp())); +} + +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + await Firebase.initializeApp(); + print('Handling a background message ${message.toMap()}'); +} + +const String tag = 'MoeExample_'; + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State with WidgetsBindingObserver { + final MoEngageFlutter _moengagePlugin = MoEngageFlutter( + 'DAO6UGZ73D9RTK8B5W96TPYN', + moEInitConfig: MoEInitConfig( + pushConfig: + PushConfig(shouldDeliverCallbackOnForegroundClick: true))); + final MoEngageGeofence _moEngageGeofence = + MoEngageGeofence('DAO6UGZ73D9RTK8B5W96TPYN'); + final MoEngageInbox _moEngageInbox = + MoEngageInbox('DAO6UGZ73D9RTK8B5W96TPYN'); + + void _onPushClick(PushCampaignData message) { + debugPrint( + '$tag Main : _onPushClick(): This is a push click callback from native to flutter. Payload $message'); + if (message.data.selfHandledPushRedirection) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => const SecondPage())); + } + } + + void _onInAppClick(ClickData message) { + debugPrint( + '$tag Main : _onInAppClick() : This is a inapp click callback from native to flutter. Payload $message'); + } + + void _onInAppShown(InAppData message) { + debugPrint( + '$tag Main : _onInAppShown() : This is a callback on inapp shown from native to flutter. Payload $message'); + } + + void _onInAppDismiss(InAppData message) { + debugPrint( + '$tag Main : _onInAppDismiss() : This is a callback on inapp dismiss from native to flutter. Payload $message'); + } + + Future _onInAppSelfHandle(SelfHandledCampaignData message) async { + debugPrint( + '$tag Main : _onInAppSelfHandle() : This is a callback on inapp self handle from native to flutter. Payload $message'); + final SelfHandledActions? action = + await asyncSelfHandledDialog(buildContext); + switch (action) { + case SelfHandledActions.Shown: + _moengagePlugin.selfHandledShown(message); + break; + case SelfHandledActions.Clicked: + _moengagePlugin.selfHandledClicked(message); + break; + case SelfHandledActions.Dismissed: + _moengagePlugin.selfHandledDismissed(message); + break; + default: + break; + } + } + + void _onPushTokenGenerated(PushTokenData pushToken) { + debugPrint( + '$tag Main : _onPushTokenGenerated() : This is callback on push token generated from native to flutter: PushToken: $pushToken'); + } + + void _permissionCallbackHandler(PermissionResultData data) { + debugPrint('$tag Permission Result: $data'); + } + + Future appendMoengageScript() { + Completer completer = Completer(); + ScriptElement script = ScriptElement(); + script.src = 'moengage_integration.js'; + script.onLoad.listen((_) { + completer.complete(); + }); + document.body?.append(script); + return completer.future; + } + + @override + void initState() { + super.initState(); + initPlatformState(); + WidgetsBinding.instance.addObserver(this); + debugPrint('$tag initState() : start '); + _moengagePlugin.setPushClickCallbackHandler(_onPushClick); + _moengagePlugin.setInAppClickHandler(_onInAppClick); + _moengagePlugin.setInAppShownCallbackHandler(_onInAppShown); + _moengagePlugin.setInAppDismissedCallbackHandler(_onInAppDismiss); + _moengagePlugin.setSelfHandledInAppHandler(_onInAppSelfHandle); + _moengagePlugin.setPushTokenCallbackHandler(_onPushTokenGenerated); + _moengagePlugin.setPermissionCallbackHandler(_permissionCallbackHandler); + _moengagePlugin.configureLogs(LogLevel.VERBOSE); + // added event listner for SDK LifeCycle + window.addEventListener("MOE_LIFECYCLE",(event) { + var detail = (event as CustomEvent).detail; + String name = detail['name']; + if(name == "SDK_INITIALIZED"){ + _moengagePlugin.initialise(); + } + }); + appendMoengageScript().then((res) { + debugPrint('$tag sdkScript Added'); + }); + debugPrint('initState() : end '); + } + + Future initPlatformState() async { + if (!mounted) return; + //Push.getTokenStream.listen(_onTokenEvent, onError: _onTokenError); + } + + late BuildContext buildContext; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + _moengagePlugin.initialise(); + } + debugPrint('Application Lifecycle Changed - $state'); + } + + @override + Widget build(BuildContext context) { + debugPrint('$tag Main : build() '); + buildContext = context; + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + actions: [ + TextButton( + onPressed: () { + _moengagePlugin.onOrientationChanged(); + }, + child: const Text('Orientation Change'), + ), + ], + ), + body: Center( + child: ListView( + children: ListTile.divideTiles(context: context, tiles: [ + ListTile( + title: const Text('GoTo Cards'), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => const CardsHome())); + }, + ), + ListTile( + title: const Text('Track Event with Attributes'), + onTap: () async { + MoEProperties details = MoEProperties(); + details + .addAttribute('temp', 567) + .addAttribute('temp1', true) + .addAttribute('temp2', 12.30) + .addAttribute('stringAttr', 'string val') + .addAttribute('attrName1', 'attrVal') + .addAttribute('attrName2', false) + .addAttribute('attrName3', 123563563) + .addAttribute('arrayAttr', [ + 'str1', + 12.8, + 'str2', + 123, + true, + {'hello': 'testing'} + ]) + .setNonInteractiveEvent() + .addAttribute('location1', MoEGeoLocation(12.1, 77.18)) + .addAttribute('location2', MoEGeoLocation(12.2, 77.28)) + .addAttribute('location3', MoEGeoLocation(12.3, 77.38)) + .addISODateTime('dateTime1', '2019-12-02T08:26:21.170Z') + .addISODateTime( + 'dateTime2', '2019-12-06T08:26:21.170Z'); + final String value = + await asyncInputDialog(context, 'Event name'); + debugPrint('$tag Main: Event name : $value'); + _moengagePlugin.trackEvent(value, details); + }), + ListTile( + title: const Text('Track Interactive Event with Attributes'), + onTap: () async { + MoEProperties details = MoEProperties(); + details + .addAttribute('temp', 567) + .addAttribute('temp1', true) + .addAttribute('temp2', 12.30) + .addAttribute('stringAttr', 'string val') + .addAttribute('attrName1', 'attrVal') + .addAttribute('attrName2', false) + .addAttribute('attrName3', 123563563) + .addAttribute('arrayAttr', [ + 'str1', + 12.8, + 'str2', + 123, + true, + {'hello': 'testing'} + ]) + .addAttribute('location1', MoEGeoLocation(12.1, 77.18)) + .addAttribute('location2', MoEGeoLocation(12.2, 77.28)) + .addAttribute('location3', MoEGeoLocation(12.3, 77.38)) + .addISODateTime('dateTime1', '2019-12-02T08:26:21.170Z') + .addISODateTime( + 'dateTime2', '2019-12-06T08:26:21.170Z'); + final String value = + await asyncInputDialog(context, 'Event name'); + debugPrint('$tag Main: Event name : $value'); + _moengagePlugin.trackEvent(value, details); + }), + ListTile( + title: const Text('Track Only Event'), + onTap: () async { + final String value = + await asyncInputDialog(context, 'Event name'); + debugPrint('$tag Main: Event name : $value'); + _moengagePlugin.trackEvent(value); + }), + ListTile( + title: const Text('Set Unique Id'), + onTap: () async { +// _moengagePlugin.setUniqueId(null); + final String value = + await asyncInputDialog(context, 'Unique Id'); + debugPrint('$tag Main: UniqueId: $value'); + _moengagePlugin.setUniqueId(value); + }), + ListTile( + title: const Text('Set UserName'), + onTap: () async { + final String value = + await asyncInputDialog(context, 'User Name'); + debugPrint('$tag Main: UserName: $value'); + _moengagePlugin.setUserName(value); + }), + ListTile( + title: const Text('Set FirstName'), + onTap: () async { + final String value = + await asyncInputDialog(context, 'First Name'); + debugPrint('$tag Main: FisrtName: $value'); + _moengagePlugin.setFirstName(value); + }), + ListTile( + title: const Text('Set LastName'), + onTap: () async { + final String value = + await asyncInputDialog(context, 'Last Name'); + debugPrint('$tag Main: Last Name: $value'); + _moengagePlugin.setLastName(value); + }), + ListTile( + title: const Text('Set Email-Id'), + onTap: () async { + final String value = + await asyncInputDialog(context, 'EmailId'); + debugPrint('$tag Main: EmailId: $value'); + _moengagePlugin.setEmail(value); + }), + ListTile( + title: const Text('Set Phone Number'), + onTap: () async { + final String value = + await asyncInputDialog(context, 'Phone Number'); + debugPrint('$tag Main: Phone Number: $value'); + _moengagePlugin.setPhoneNumber(value); + }), + ListTile( + title: const Text('Set Gender'), + onTap: () { + _moengagePlugin.setGender(MoEGender.female); + }), + ListTile( + title: const Text('Set Location'), + onTap: () { + _moengagePlugin.setLocation(MoEGeoLocation(23.1, 21.2)); + }), + ListTile( + title: const Text('Set Birthday'), + onTap: () { + _moengagePlugin.setBirthDate('2019-12-02T08:26:21.170Z'); + }), + ListTile( + title: const Text('Set Alias'), + onTap: () async { + final String value = await asyncInputDialog(context, 'Alias'); + debugPrint('$tag Main: Alias : $value'); + _moengagePlugin.setAlias(value); + }, + ), + ListTile( + title: const Text('Set Custom User Attributes'), + onTap: () { + const num number = 15.4567; + _moengagePlugin.setUserAttribute('userAttr-bool', true); + _moengagePlugin.setUserAttribute('userAttr-int', 1443322); + _moengagePlugin.setUserAttribute('userAttr-Double', 45.4567); + _moengagePlugin.setUserAttribute('userAttr-Number', number); + _moengagePlugin.setUserAttribute( + 'userAttr-String', 'This is a string'); + _moengagePlugin + .setUserAttribute('userAttr-array-int', [1, 2, 3, 4, 5]); + _moengagePlugin.setUserAttribute( + 'userAttr-array-Double', [1.0, 1.5, 0.01, 5.45]); + _moengagePlugin.setUserAttribute( + 'userAttr-array-Number', [1.0, 1, 0.01, 5.45]); + _moengagePlugin.setUserAttribute('userAttr-array-String', + ['This', 'is', 'an', 'array', 'of', 'strings']); + }, + ), + ListTile( + title: const Text('Set UserAttribute Timestamp'), + onTap: () { + _moengagePlugin.setUserAttributeIsoDate( + 'timeStamp', '2019-12-02T08:26:21.170Z'); + }, + ), + ListTile( + title: const Text('Set UserAttribute Location'), + onTap: () { + _moengagePlugin.setUserAttributeLocation( + 'locationAttr', MoEGeoLocation(72.8, 53.2)); + }, + ), + ListTile( + title: const Text('App Status - Install'), + onTap: () { + _moengagePlugin.setAppStatus(MoEAppStatus.install); + }), + ListTile( + title: const Text('App Status - Update'), + onTap: () { + _moengagePlugin.setAppStatus(MoEAppStatus.update); + }), + ListTile( + title: const Text('iOS -- Register For Push'), + onTap: () { + _moengagePlugin.registerForPushNotification(); + }), + ListTile( + title: const Text('Start Geofence Monitoring'), + onTap: () { + debugPrint('Start GeoFence Monitoring - Flutter'); + _moEngageGeofence.startGeofenceMonitoring(); + }), + ListTile( + title: const Text('Stop Geofence Monitoring'), + onTap: () { + debugPrint('Stop GeoFence Monitoring - Flutter'); + _moEngageGeofence.stopGeofenceMonitoring(); + }), + ListTile( + title: const Text('Request Location Permission'), + onTap: () async { + Map statuses = + await [Permission.locationAlways].request(); + debugPrint(statuses.toString()); + }, + ), + ListTile( + title: const Text('Show InApp'), + onTap: () { + _moengagePlugin.showInApp(); + }), + ListTile( + title: const Text('Show Self Handled InApp'), + onTap: () { + buildContext = context; + _moengagePlugin.getSelfHandledInApp(); + }), + ListTile( + title: const Text('Set InApp Contexts'), + onTap: () { + _moengagePlugin.setCurrentContext(['HOME', 'SETTINGS']); + }), + ListTile( + title: const Text('Reset Contexts'), + onTap: () { + _moengagePlugin.resetCurrentContext(); + }), + ListTile( + title: const Text('Android -- FCM Push Token'), + onTap: () { +// Token passed here is just for illustration purposes. Please pass the actual token instead. +// _moengagePlugin.passFCMPushToken(null); + _moengagePlugin.passFCMPushToken( + 'dTFauhaKRgiRpBk_evwffB:APA91bEGxaY53AlYMpjqxgNd7GK_dWlwrqvKLF7MvaAeVFYyDYyXlJ7JuoItnVir0zLl53TXZcStdeUvGpeorhEfOtPk6ML4anpwoOEak16bhS0455X-yFY5VqZdrZ58dfaA16wyXbhH'); + }), + ListTile( + title: const Text('Android -- PushKit Push Token'), + onTap: () { + // Token passed here is just for illustration purposes. Please pass the actual token instead. + _moengagePlugin.passPushKitPushToken( + 'IQAAAACy0T43AABSrIoiO4BN6XNORkptaWgyxTTEcIS9EgA1PUeNdYcAeBP6Ea-X6oIsWv5j7HKA8Hdna_JBMpNiVp_B8xR8HYEHC2Yw5yhE69AyaQ'); + }), + ListTile( + title: const Text('Android -- FCM Push Payload'), + onTap: () { + // this payload is only for illustration purpose. Please pass the actual push payload. + Map pushPayload = {}; + pushPayload.putIfAbsent('push_from', () => 'moengage'); + pushPayload.putIfAbsent('gcm_title', () => 'Title'); + pushPayload.putIfAbsent( + 'moe_app_id', () => 'DAO6UGZ73D9RTK8B5W96TPYN'); + pushPayload.putIfAbsent( + 'gcm_notificationType', () => 'normal notification'); + pushPayload.putIfAbsent('gcm_alert', () => 'Message'); + + pushPayload.putIfAbsent('gcm_campaign_id', + () => DateTime.now().millisecondsSinceEpoch.toString()); + pushPayload.putIfAbsent('gcm_activityName', + () => 'com.moengage.sampleapp.MainActivity'); + _moengagePlugin.passFCMPushPayload(pushPayload); + }), + ListTile( + title: const Text('Enable data tracking'), + onTap: () { + _moengagePlugin.enableDataTracking(); + }), + ListTile( + title: const Text('Disable data tracking'), + onTap: () { + _moengagePlugin.disableDataTracking(); + }), + ListTile( + title: const Text('Logout'), + onTap: () { + _moengagePlugin.logout(); + }, + ), + ListTile( + title: const Text('Inbox: Un-Clicked Count'), + onTap: () async { + int count = await _moEngageInbox.getUnClickedCount(); + debugPrint('$tag Main : Un-clicked Count $count'); + }, + ), + ListTile( + title: const Text('Inbox: Get all messages'), + onTap: () async { + InboxData? data = await _moEngageInbox.fetchAllMessages(); + if (data != null) { + debugPrint( + '$tag Main : Inbox Messages count: ${data.messages.length}'); + if (data.messages.isNotEmpty) { + for (final InboxMessage message in data.messages) { + debugPrint('$tag Main : Inbox Messages $message'); + } + } + } + }, + ), + ListTile( + title: const Text('Inbox: Track all messages'), + onTap: () async { + InboxData? data = await _moEngageInbox.fetchAllMessages(); + if (data != null) { + debugPrint( + '$tag Main : Inbox Messages count: ${data.messages.length}'); + if (data.messages.isNotEmpty) { + for (final InboxMessage message in data.messages) { + debugPrint( + '$tag Main : Tracking inbox message: $message'); + _moEngageInbox.trackMessageClicked(message); + } + } + } + }, + ), + ListTile( + title: const Text('Inbox: Delete all messages'), + onTap: () async { + InboxData? data = await _moEngageInbox.fetchAllMessages(); + if (data != null) { + debugPrint( + '$tag Main : Inbox Messages count: ${data.messages.length}'); + if (data.messages.isNotEmpty) { + for (final InboxMessage message in data.messages) { + debugPrint( + '$tag Main : Deleting inbox message: $message'); + _moEngageInbox.deleteMessage(message); + } + } + } + }, + ), + ListTile( + title: const Text('Enable Sdk'), + onTap: () async { + _moengagePlugin.enableSdk(); + }, + ), + ListTile( + title: const Text('Disable Sdk'), + onTap: () async { + _moengagePlugin.disableSdk(); + }, + ), + ListTile( + title: const Text('Android- Enable Android Id'), + onTap: () async { + _moengagePlugin.enableAndroidIdTracking(); + }, + ), + ListTile( + title: const Text('Android- Disable Android Id'), + onTap: () async { + _moengagePlugin.disableAndroidIdTracking(); + }, + ), + ListTile( + title: const Text('Android- Enable Ad Id'), + onTap: () async { + _moengagePlugin.enableAdIdTracking(); + }, + ), + ListTile( + title: const Text('Android- Disable Ad Id'), + onTap: () async { + _moengagePlugin.disableAdIdTracking(); + }, + ), + ListTile( + title: const Text('Android- Navigate to Settings'), + onTap: () async { + _moengagePlugin.navigateToSettingsAndroid(); + }, + ), + ListTile( + title: const Text('Android- Request Push Permission'), + onTap: () async { + _moengagePlugin.requestPushPermissionAndroid(); + }, + ), + ListTile( + title: const Text('Android- Mock push permission granted'), + onTap: () async { + _moengagePlugin.pushPermissionResponseAndroid(true); + }, + ), + ListTile( + title: const Text('Android- Mock push permission denied'), + onTap: () async { + _moengagePlugin.pushPermissionResponseAndroid(false); + }, + ), + ListTile( + title: const Text('Update Push Permission Request Count'), + onTap: () async { + final String value = + await asyncInputDialog(context, 'Push Permission Count'); + final int pushPermissionCount = int.tryParse(value) ?? 0; + _moengagePlugin.updatePushPermissionRequestCountAndroid( + pushPermissionCount); + }, + ), + ListTile( + title: const Text('Enable Device Id Tracking'), + onTap: () async { + _moengagePlugin.enableDeviceIdTracking(); + }, + ), + ListTile( + title: const Text('Disable Device Id Tracking'), + onTap: () async { + _moengagePlugin.disableDeviceIdTracking(); + }, + ), + ListTile( + title: const Text('Delete User - (Android Only)'), + onTap: () { + _moengagePlugin.deleteUser().then((value) { + debugPrint('User Deletion Result: ${value.isSuccess}'); + }).catchError((onError) { + debugPrint('Error Occurred while Deleting User Data'); + }); + }, + ), + ]).toList(), + ), + ), + ), + ); + } +} diff --git a/flutter-sample/example/lib/second_page.dart b/flutter-sample/example/lib/second_page.dart new file mode 100644 index 00000000..85a7eaf3 --- /dev/null +++ b/flutter-sample/example/lib/second_page.dart @@ -0,0 +1,19 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; + +class SecondPage extends StatelessWidget { + const SecondPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Second Page'), + ), + body: const Center( + child: Text('Page 2'), + ), + ); + } +} diff --git a/flutter-sample/example/lib/utils.dart b/flutter-sample/example/lib/utils.dart new file mode 100644 index 00000000..f3a75402 --- /dev/null +++ b/flutter-sample/example/lib/utils.dart @@ -0,0 +1,75 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; + +import 'main.dart'; + +Future asyncInputDialog(BuildContext context, String prompt, + {TextInputType textInputType = TextInputType.text}) async { + String teamName = ''; + await showDialog( + context: context, + barrierDismissible: + false, // dialog is dismissible with a tap on the barrier + builder: (BuildContext context) { + return AlertDialog( + content: Row( + children: [ + Expanded( + child: TextField( + keyboardType: textInputType, + autofocus: true, + decoration: InputDecoration(labelText: prompt), + onChanged: (String value) { + teamName = value; + }, + )) + ], + ), + actions: [ + TextButton( + child: const Text('Ok'), + onPressed: () { + Navigator.of(context).pop(teamName); + }, + ), + ], + ); + }, + ); + return teamName; +} + +enum SelfHandledActions { Shown, Clicked, Dismissed } + +Future asyncSelfHandledDialog(BuildContext context) async { + debugPrint('$tag asyncSelfHandledDialog'); + return await showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Choose action'), + children: [ + SimpleDialogOption( + onPressed: () { + Navigator.pop(context, SelfHandledActions.Shown); + }, + child: const Text('Shown'), + ), + SimpleDialogOption( + onPressed: () { + Navigator.pop(context, SelfHandledActions.Clicked); + }, + child: const Text('Clicked'), + ), + SimpleDialogOption( + onPressed: () { + Navigator.pop(context, SelfHandledActions.Dismissed); + }, + child: const Text('Dismissed'), + ), + ], + ); + }); +} diff --git a/flutter-sample/example/pubspec.yaml b/flutter-sample/example/pubspec.yaml new file mode 100644 index 00000000..998f1e8d --- /dev/null +++ b/flutter-sample/example/pubspec.yaml @@ -0,0 +1,73 @@ +name: moengage_flutter_example +description: Demonstrates how to use the moengage_flutter plugin. +publish_to: 'none' +version: 1.0.0+1 +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: "^3.0.0" + +dependencies: + flutter: + sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + permission_handler: + + moengage_flutter: + moengage_geofence: + moengage_inbox: + moengage_cards: + flutter_html: 3.0.0-beta.2 + flutter_math_fork: 0.6.0 + intl: ^0.18.1 + url_launcher: 6.0.0 + collection: + firebase_messaging: ^14.2.3 + firebase_core: ^2.6.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/flutter-sample/example/pubspec_overrides.yaml b/flutter-sample/example/pubspec_overrides.yaml new file mode 100644 index 00000000..28dee862 --- /dev/null +++ b/flutter-sample/example/pubspec_overrides.yaml @@ -0,0 +1,35 @@ +dependency_overrides: + moengage_flutter: + path: ../core/moengage_flutter/ + moengage_flutter_android: + path: ../core/moengage_flutter_android/ + moengage_flutter_ios: + path: ../core/moengage_flutter_ios/ + moengage_flutter_web: + path: ../core/moengage_flutter_web/ + moengage_flutter_platform_interface: + path: ../core/moengage_flutter_platform_interface/ + moengage_geofence: + path: ../geofence/moengage_geofence/ + moengage_geofence_android: + path: ../geofence/moengage_geofence_android/ + moengage_geofence_ios: + path: ../geofence/moengage_geofence_ios/ + moengage_geofence_platform_interface: + path: ../geofence/moengage_geofence_platform_interface/ + moengage_inbox: + path: ../inbox/moengage_inbox/ + moengage_inbox_android: + path: ../inbox/moengage_inbox_android/ + moengage_inbox_ios: + path: ../inbox/moengage_inbox_ios/ + moengage_inbox_platform_interface: + path: ../inbox/moengage_inbox_platform_interface/ + moengage_cards: + path: ../cards/moengage_cards/ + moengage_cards_android: + path: ../cards/moengage_cards_android/ + moengage_cards_ios: + path: ../cards/moengage_cards_ios/ + moengage_cards_platform_interface: + path: ../cards/moengage_cards_platform_interface/ \ No newline at end of file diff --git a/flutter-sample/example/test/widget_test.dart b/flutter-sample/example/test/widget_test.dart new file mode 100644 index 00000000..6fbf2750 --- /dev/null +++ b/flutter-sample/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:moengage_flutter_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && widget.data?.startsWith('Running on:') == true, + ), + findsOneWidget, + ); + }); +} diff --git a/flutter-sample/example/web/favicon.png b/flutter-sample/example/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/flutter-sample/example/web/favicon.png differ diff --git a/flutter-sample/example/web/icons/Icon-192.png b/flutter-sample/example/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/flutter-sample/example/web/icons/Icon-192.png differ diff --git a/flutter-sample/example/web/icons/Icon-512.png b/flutter-sample/example/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/flutter-sample/example/web/icons/Icon-512.png differ diff --git a/flutter-sample/example/web/index.html b/flutter-sample/example/web/index.html new file mode 100644 index 00000000..1460b5e9 --- /dev/null +++ b/flutter-sample/example/web/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + + diff --git a/flutter-sample/example/web/manifest.json b/flutter-sample/example/web/manifest.json new file mode 100644 index 00000000..8c012917 --- /dev/null +++ b/flutter-sample/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/flutter-sample/example/web/moengage_integration.js b/flutter-sample/example/web/moengage_integration.js new file mode 100644 index 00000000..684ac05c --- /dev/null +++ b/flutter-sample/example/web/moengage_integration.js @@ -0,0 +1,6 @@ +(function(i,s,o,g,r,a,m,n){i.moengage_object=r;t={};q=function(f){return function(){(i.moengage_q=i.moengage_q||[]).push({f:f,a:arguments})}};f=['track_event','add_user_attribute','add_first_name','add_last_name','add_email','add_mobile','add_user_name','add_gender','add_birthday','destroy_session','add_unique_user_id','moe_events','call_web_push','track','location_type_attribute'],h={onsite:["getData","registerCallback"]};for(k in f){t[f[k]]=q(f[k])}for(k in h)for(l in h[k]){null==t[k]&&(t[k]={}),t[k][h[k][l]]=q(k+"."+h[k][l])}a=s.createElement(o);m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m);i.moe=i.moe||function(){n=arguments[0];return t};a.onload=function(){if(n){i[r]=moe(n)}}})(window,document,'script','https://cdn.moengage.com/webpush/moe_webSdk.min.latest.js','Moengage') + +Moengage = moe({ + app_id:"DAO6UGZ73D9RTK8B5W96TPYN", + debug_logs: 0 +}); \ No newline at end of file diff --git a/flutter-sample/example/web/serviceworker.js b/flutter-sample/example/web/serviceworker.js new file mode 100644 index 00000000..3b8c2c5e --- /dev/null +++ b/flutter-sample/example/web/serviceworker.js @@ -0,0 +1 @@ +importScripts("//cdn.moengage.com/webpush/releases/serviceworker_cdn.min.latest.js?date="); diff --git a/flutter-sample/geofence/.DS_Store b/flutter-sample/geofence/.DS_Store new file mode 100644 index 00000000..5335ed88 Binary files /dev/null and b/flutter-sample/geofence/.DS_Store differ diff --git a/flutter-sample/geofence/moengage_geofence/CHANGELOG.md b/flutter-sample/geofence/moengage_geofence/CHANGELOG.md new file mode 100644 index 00000000..5631e806 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/CHANGELOG.md @@ -0,0 +1,58 @@ +# MoEngage Geofence Plugin + +# 07-12-2023 + +## 2.1.0 +- Updated Minimum Supported `moengage_flutter` version to `6.1.0` +- Android + - Native SDK updated to support version `12.10.01` and above. + +# 13-09-2023 + +## 2.0.0 +- Federated Plugin Implementation +- Breaking Change: Use import 'package:moengage_geofence/moengage_goefence.dart'; to import any files in the `moengage_geofence` package. Remove the existing import related to `moengage_geofence` + +# 19-07-2023 + +## 1.6.0 +- iOS + - MoEngageGeofence SDK version Updated to `5.10.0` + +# 31-05-2023 + +## 1.5.0 +- Android + - Compile SDK Version Updated to 33 + - Native SDK updated to support version `12.9.00` and above. +- iOS + - MoEngageGeofence SDK version updated to `~>5.8.0`. + +# 21-02-2023 + +## 1.4.0 +- Android + - Support for GeoFence Start/Stop API +- iOS + - Added support for `stopGeofenceMonitoring` API. + +# 08-02-2023 + +## 1.3.0 +- Security improvement: controlled logging for release, debug and profile mode +- MoEngageGeofence SDK version updated to `~>5.4.0`. + +# 23-01-2023 + +## 1.2.0 +- MoEngageGeofence SDK version updated to `~>5.2.0`. + +# 27-10-2022 + +## 1.1.0 +- MoEngageGeofence SDK version updated to `~>4.4.0`. + +## 27.09.2022 + +### 1.0.0 +- MoEngageGeofence SDK version updated to `~>4.3.0`. \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence/LICENSE b/flutter-sample/geofence/moengage_geofence/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence/README.md b/flutter-sample/geofence/moengage_geofence/README.md new file mode 100644 index 00000000..3c857377 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/README.md @@ -0,0 +1,20 @@ +# MoEngage Geofence Plugin + +Geofence Plugin for MoEngage Platform + +## SDK Installation + +To add the MoEngage Geofence SDK to your application, edit your application's `pubspec.yaml` file and add the below dependency to it: + +![Download](https://img.shields.io/pub/v/moengage_geofence.svg) + +```yaml +dependencies: + moengage_geofence: $latestSdkVersion +``` +replace `$latestSdkVersion` with the latest SDK version. + +Run flutter packages get to install the SDK. + + Note: This plugin is dependent on `moengage_flutter` plugin. Make sure you have installed the `moengage_flutter + ` make sure you have installed the `moengage_flutter` plugin as well. diff --git a/flutter-sample/geofence/moengage_geofence/example/README.md b/flutter-sample/geofence/moengage_geofence/example/README.md new file mode 100644 index 00000000..751dfdfa --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/example/README.md @@ -0,0 +1,3 @@ +# moengage_geofence_example + +Demonstrates how to use the moengage_geofence plugin. diff --git a/flutter-sample/geofence/moengage_geofence/lib/moengage_geofence.dart b/flutter-sample/geofence/moengage_geofence/lib/moengage_geofence.dart new file mode 100644 index 00000000..6bad9e2a --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/lib/moengage_geofence.dart @@ -0,0 +1 @@ +export 'src/moengage_geofence.dart'; diff --git a/flutter-sample/geofence/moengage_geofence/lib/src/moengage_geofence.dart b/flutter-sample/geofence/moengage_geofence/lib/src/moengage_geofence.dart new file mode 100644 index 00000000..a95c4553 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/lib/src/moengage_geofence.dart @@ -0,0 +1,25 @@ +import 'package:moengage_flutter/moengage_flutter.dart' show Logger; +import 'package:moengage_geofence_platform_interface/moengage_geofence_platform_interface.dart'; + +/// Helper Class to interact with MoEngage GeoFence Feature +class MoEngageGeofence { + /// Constructor for [MoEngageGeofence] + MoEngageGeofence(this.appId); + + /// MoEngage App ID + late String appId; + + final String _tag = '${moduleTagGeofence}MoEngageGeofence'; + + /// Starts Geofence Monitoring + void startGeofenceMonitoring() { + Logger.v('$_tag Starting GeoFence Monitoring'); + MoEngageGeofencePlatform.instance.startGeofenceMonitoring(appId); + } + + /// Stops Geofence Monitoring + void stopGeofenceMonitoring() { + Logger.v('$_tag Stopping GeoFence Monitoring'); + MoEngageGeofencePlatform.instance.stopGeofenceMonitoring(appId); + } +} diff --git a/flutter-sample/geofence/moengage_geofence/pubspec.yaml b/flutter-sample/geofence/moengage_geofence/pubspec.yaml new file mode 100644 index 00000000..4db3915f --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/pubspec.yaml @@ -0,0 +1,28 @@ +name: moengage_geofence +description: Flutter Plugin for using Geofence feature of MoEngage Platform. +version: 2.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.1.0 + moengage_geofence_android: ^1.1.0 + moengage_geofence_ios: ^1.1.0 + moengage_geofence_platform_interface: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + default_package: moengage_geofence_android + ios: + default_package: moengage_geofence_ios \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence/pubspec_overrides.yaml b/flutter-sample/geofence/moengage_geofence/pubspec_overrides.yaml new file mode 100644 index 00000000..d0ad3dc4 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/pubspec_overrides.yaml @@ -0,0 +1,17 @@ +dependency_overrides: + moengage_geofence_platform_interface: + path: ../moengage_geofence_platform_interface/ + moengage_geofence_android: + path: ../moengage_geofence_android + moengage_geofence_ios: + path: ../moengage_geofence_ios + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence/test/moengage_geofence_test.dart b/flutter-sample/geofence/moengage_geofence/test/moengage_geofence_test.dart new file mode 100644 index 00000000..1f508431 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence/test/moengage_geofence_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); +} diff --git a/flutter-sample/geofence/moengage_geofence_android/CHANGELOG.md b/flutter-sample/geofence/moengage_geofence_android/CHANGELOG.md new file mode 100644 index 00000000..2e2aa287 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/CHANGELOG.md @@ -0,0 +1,13 @@ +# MoEngage Geofence Android Plugin + +# 07-12-2023 + +# 1.1.0 +- Add support for AGP `8.0.2` and above +- Upgrade Kotlin Version to `1.7.10` +- Support for `geofence` version `3.4.0` and above + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/LICENSE b/flutter-sample/geofence/moengage_geofence_android/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/README.md b/flutter-sample/geofence/moengage_geofence_android/README.md new file mode 100644 index 00000000..79ba7628 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/README.md @@ -0,0 +1,15 @@ +# moengage\_geofence\_android + +The Android implementation of [`moengage_geofence`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_geofence` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_geofence +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/android/.gitignore b/flutter-sample/geofence/moengage_geofence_android/android/.gitignore new file mode 100644 index 00000000..26659750 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/android/build.gradle b/flutter-sample/geofence/moengage_geofence_android/android/build.gradle new file mode 100644 index 00000000..92562692 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/build.gradle @@ -0,0 +1,57 @@ +group 'com.moengage.moengage_geofence' +version '1.0' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.0.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdk 33 + namespace "com.moengage.flutter.geofence" + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 21 + } + lintOptions { + disable 'InvalidPackage' + } + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compileOnly("com.moengage:moe-android-sdk:12.10.01") + compileOnly("com.moengage:geofence:3.4.0") + implementation("com.moengage:plugin-base-geofence:1.2.0") +} + +apply from: file("./user-agent.gradle") \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/android/gradle.properties b/flutter-sample/geofence/moengage_geofence_android/android/gradle.properties new file mode 100644 index 00000000..87c57042 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/android/gradle/wrapper/gradle-wrapper.properties b/flutter-sample/geofence/moengage_geofence_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a403c412 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/android/settings.gradle b/flutter-sample/geofence/moengage_geofence_android/android/settings.gradle new file mode 100644 index 00000000..153bd1f0 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'moengage_geofence_flutter' \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/android/src/main/AndroidManifest.xml b/flutter-sample/geofence/moengage_geofence_android/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9f6b0bab --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/flutter-sample/geofence/moengage_geofence_android/android/src/main/kotlin/com/moengage/flutter/geofence/Constants.kt b/flutter-sample/geofence/moengage_geofence_android/android/src/main/kotlin/com/moengage/flutter/geofence/Constants.kt new file mode 100644 index 00000000..2eb71bc0 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/src/main/kotlin/com/moengage/flutter/geofence/Constants.kt @@ -0,0 +1,6 @@ +package com.moengage.flutter.geofence + +internal const val CHANNEL_NAME = "com.moengage/geofence" + +internal const val METHOD_NAME_START_GEOFENCE_MONITORING = "startGeofenceMonitoring" +internal const val METHOD_NAME_STOP_GEOFENCE_MONITORING = "stopGeofenceMonitoring" \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/android/src/main/kotlin/com/moengage/flutter/geofence/MoEngageGeofencePlugin.kt b/flutter-sample/geofence/moengage_geofence_android/android/src/main/kotlin/com/moengage/flutter/geofence/MoEngageGeofencePlugin.kt new file mode 100644 index 00000000..a474b4ad --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/src/main/kotlin/com/moengage/flutter/geofence/MoEngageGeofencePlugin.kt @@ -0,0 +1,83 @@ +package com.moengage.flutter.geofence + +import android.content.Context +import androidx.annotation.NonNull +import com.moengage.core.LogLevel +import com.moengage.core.internal.logger.Logger +import com.moengage.geofence.MoEGeofenceHelper +import com.moengage.plugin.base.geofence.internal.GeofencePluginHelper +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** MoEngage Geofence Plugin */ +class MoEngageGeofencePlugin : FlutterPlugin, MethodCallHandler { + private lateinit var channel: MethodChannel + + private val tag = "MoEngageGeofencePlugin" + private lateinit var context: Context + private val geofenceHelper = GeofencePluginHelper() + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + } + + @Suppress("SENSELESS_COMPARISON") + override fun onMethodCall(call: MethodCall, result: Result) { + try { + if (call == null) { + Logger.print(LogLevel.ERROR) { + "$tag onMethodCall() : MethodCall instance is null cannot proceed further." + } + return + } + if (context == null) { + Logger.print(LogLevel.ERROR) { + "$tag onMethodCall() : Context is null cannot proceed further." + } + return + } + Logger.print { "$tag onMethodCall() : Method: ${call.method}" } + when (call.method) { + METHOD_NAME_START_GEOFENCE_MONITORING -> startGeofenceMonitoring(call) + METHOD_NAME_STOP_GEOFENCE_MONITORING -> stopGeofenceMonitoring(call) + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag onMethodCall() : " } + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + private fun startGeofenceMonitoring(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag startGeofenceMonitoring() : $payload" } + geofenceHelper.startGeofenceMonitoring(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { + "$tag startGeofenceMonitoring() : " + } + } + } + + private fun stopGeofenceMonitoring(methodCall: MethodCall) { + try { + if (methodCall.arguments == null) return + val payload = methodCall.arguments.toString() + Logger.print { "$tag stopGeofenceMonitoring() : $payload" } + geofenceHelper.stopGeofenceMonitoring(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { + "$tag stopGeofenceMonitoring() : " + } + } + } +} diff --git a/flutter-sample/geofence/moengage_geofence_android/android/user-agent.gradle b/flutter-sample/geofence/moengage_geofence_android/android/user-agent.gradle new file mode 100644 index 00000000..017a15b9 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/android/user-agent.gradle @@ -0,0 +1,19 @@ +import java.util.regex.Matcher +import java.util.regex.Pattern + +String libraryVersionName = "UNKNOWN" +File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml') + +if (pubspec.exists()) { + String yaml = pubspec.text + // Using \s*['|"]?([^\n|'|"]*)['|"]? to extract version number. + Matcher versionMatcher = Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", Pattern.MULTILINE).matcher(yaml) + if (versionMatcher.find()) libraryVersionName = versionMatcher.group(1).replaceAll("\\+", "-") +} + +android { + defaultConfig { + // BuildConfig.VERSION_NAME + buildConfigField("String", 'MOENGAGE_GEOFENCE_FLUTTER_LIBRARY_VERSION', "\"${libraryVersionName}\"") + } +} \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_android/lib/moengage_geofence_android.dart b/flutter-sample/geofence/moengage_geofence_android/lib/moengage_geofence_android.dart new file mode 100644 index 00000000..70511685 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/lib/moengage_geofence_android.dart @@ -0,0 +1,37 @@ +import 'package:flutter/services.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' + show Logger, getAccountMeta; +import 'package:moengage_geofence_platform_interface/moengage_geofence_platform_interface.dart'; + +/// The Android implementation of [MoEngageGeofencePlatform]. +class MoEngageGeofenceAndroid extends MoEngageGeofencePlatform { + final String _tag = '${moduleTagGeofence}MoEAndroidGeofence'; + + final MethodChannel _channel = const MethodChannel(geoFenceChannelName); + + /// Registers this class as the default instance of [MoEngageGeofencePlatform] + static void registerWith() { + Logger.v('Registering MoEngageGeofenceAndroid with Platform Interface'); + MoEngageGeofencePlatform.instance = MoEngageGeofenceAndroid(); + } + + @override + void startGeofenceMonitoring(String appId) { + try { + _channel.invokeMethod( + methodStartGeofenceMonitoring, getAccountMeta(appId)); + } catch (e) { + Logger.e('$_tag Error: startGeofenceMonitoring() : $e'); + } + } + + @override + void stopGeofenceMonitoring(String appId) { + try { + _channel.invokeMethod( + methodStopGeofenceMonitoring, getAccountMeta(appId)); + } catch (e) { + Logger.e('$_tag Error: stopGeofenceMonitoring() : $e'); + } + } +} diff --git a/flutter-sample/geofence/moengage_geofence_android/pubspec.yaml b/flutter-sample/geofence/moengage_geofence_android/pubspec.yaml new file mode 100644 index 00000000..2423d57b --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/pubspec.yaml @@ -0,0 +1,28 @@ +name: moengage_geofence_android +description: Android implementation of the moengage_geofence plugin +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_geofence + platforms: + android: + package: com.moengage.flutter.geofence + pluginClass: MoEngageGeofencePlugin + dartPluginClass: MoEngageGeofenceAndroid + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.1.0 + moengage_geofence_platform_interface: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 diff --git a/flutter-sample/geofence/moengage_geofence_android/pubspec_overrides.yaml b/flutter-sample/geofence/moengage_geofence_android/pubspec_overrides.yaml new file mode 100644 index 00000000..2c46f139 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_android/pubspec_overrides.yaml @@ -0,0 +1,13 @@ +dependency_overrides: + moengage_geofence_platform_interface: + path: ../moengage_geofence_platform_interface/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web + moengage_flutter: + path: ../../core/moengage_flutter/ \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_ios/CHANGELOG.md b/flutter-sample/geofence/moengage_geofence_ios/CHANGELOG.md new file mode 100644 index 00000000..20d0d3ae --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/CHANGELOG.md @@ -0,0 +1,11 @@ +# MoEngage Geofence iOS Plugin + +# 01-12-2023 + +## 1.1.0 +- Updated MoEngageGeofence to 5.13.0 + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_ios/LICENSE b/flutter-sample/geofence/moengage_geofence_ios/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_ios/README.md b/flutter-sample/geofence/moengage_geofence_ios/README.md new file mode 100644 index 00000000..313a7048 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/README.md @@ -0,0 +1,15 @@ +# moengage\_geofence\_ios + +The iOS implementation of [`moengage_geofence`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_geofence` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_geofence +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_ios/ios/.gitignore b/flutter-sample/geofence/moengage_geofence_ios/ios/.gitignore new file mode 100644 index 00000000..0c885071 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_ios/ios/Assets/.gitkeep b/flutter-sample/geofence/moengage_geofence_ios/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageFlutterGeofence.swift b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageFlutterGeofence.swift new file mode 100644 index 00000000..a805d562 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageFlutterGeofence.swift @@ -0,0 +1,24 @@ +import Flutter +import UIKit +import MoEngagePluginGeofence + +public class MoEngageFlutterGeofence: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: MoEngageFlutterGeofenceConstants.channel, binaryMessenger: registrar.messenger()) + let instance = MoEngageFlutterGeofence() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let payload = call.arguments as? [String: Any] else { return } + + switch call.method { + case MoEngageFlutterGeofenceConstants.startGeofenceMonitoring : + MoEngagePluginGeofenceBridge.sharedInstance.startGeofenceMonitoring(payload) + case MoEngageFlutterGeofenceConstants.stopGeofenceMonitoring : + MoEngagePluginGeofenceBridge.sharedInstance.stopGeofenceMonitoring(payload) + default: + break + } + } +} diff --git a/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageFlutterGeofenceConstants.swift b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageFlutterGeofenceConstants.swift new file mode 100644 index 00000000..6a1628bc --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageFlutterGeofenceConstants.swift @@ -0,0 +1,14 @@ +// +// MoEngageFlutterGeofenceConstants.swift +// moengage_geofence +// +// Created by Rakshitha on 02/09/22. +// + +import Foundation + +struct MoEngageFlutterGeofenceConstants { + static let channel = "com.moengage/geofence" + static let startGeofenceMonitoring = "startGeofenceMonitoring" + static let stopGeofenceMonitoring = "stopGeofenceMonitoring" +} diff --git a/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageGeofencePlugin.h b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageGeofencePlugin.h new file mode 100644 index 00000000..c929f80b --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageGeofencePlugin.h @@ -0,0 +1,4 @@ +#import + +@interface MoEngageGeofencePlugin : NSObject +@end diff --git a/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageGeofencePlugin.m b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageGeofencePlugin.m new file mode 100644 index 00000000..7c3b3af3 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/ios/Classes/MoEngageGeofencePlugin.m @@ -0,0 +1,15 @@ +#import "MoengageGeofencePlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "moengage_geofence_ios-Swift.h" +#endif + +@implementation MoEngageGeofencePlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [MoEngageFlutterGeofence registerWithRegistrar:registrar]; +} +@end diff --git a/flutter-sample/geofence/moengage_geofence_ios/ios/moengage_geofence_ios.podspec b/flutter-sample/geofence/moengage_geofence_ios/ios/moengage_geofence_ios.podspec new file mode 100644 index 00000000..37e86fd8 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/ios/moengage_geofence_ios.podspec @@ -0,0 +1,21 @@ +require 'yaml' +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +libraryVersion = pubspec['version'].gsub('+', '-') + +Pod::Spec.new do |s| + s.name = 'moengage_geofence_ios' + s.version = libraryVersion + s.summary = 'A flutter plugin to manage geofence for MoEngage iOS SDK.' + s.description = <<-DESC + A flutter plugin for MoEngage iOS SDK + DESC + s.homepage = 'https://www.moengage.com/' + s.license = { :file => '../LICENSE' } + s.author = { 'MoEngage Inc.' => 'mobiledevs@moengage.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + s.swift_version = '5.0' + s.dependency 'MoEngagePluginGeofence', '~> 2.5.1' +end diff --git a/flutter-sample/geofence/moengage_geofence_ios/lib/moengage_geofence_ios.dart b/flutter-sample/geofence/moengage_geofence_ios/lib/moengage_geofence_ios.dart new file mode 100644 index 00000000..9b887b1b --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/lib/moengage_geofence_ios.dart @@ -0,0 +1,48 @@ +import 'package:flutter/services.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' + show Logger, getAccountMeta; +import 'package:moengage_geofence_platform_interface/moengage_geofence_platform_interface.dart'; + +/// IOS implementation of [MoEngageGeofencePlatform]. +class MoEngageGeofenceIOS extends MoEngageGeofencePlatform { + /// [MoEngageGeofenceIOS] Constructor + MoEngageGeofenceIOS(); + + final String _tag = '${moduleTagGeofence}MoEngageGeofenceIOS'; + + final MethodChannel _channel = const MethodChannel(geoFenceChannelName); + + /// Registers this class as the default instance of [MoEngageGeofencePlatform] + static void registerWith() { + Logger.v('Registering MoEngageGeofenceIOS with Platform Interface'); + MoEngageGeofencePlatform.instance = MoEngageGeofenceIOS(); + } + + @override + void startGeofenceMonitoring(String appId) { + try { + _channel.invokeMethod( + methodStartGeofenceMonitoring, + getAccountMeta(appId), + ); + } catch (e, stacktrace) { + Logger.e( + '$_tag Error: startGeofenceMonitoring() : ', + error: e, + stackTrace: stacktrace, + ); + } + } + + @override + void stopGeofenceMonitoring(String appId) { + try { + _channel.invokeMethod( + methodStopGeofenceMonitoring, + getAccountMeta(appId), + ); + } catch (e) { + Logger.e('$_tag Error: stopGeofenceMonitoring() : $e'); + } + } +} diff --git a/flutter-sample/geofence/moengage_geofence_ios/pubspec.yaml b/flutter-sample/geofence/moengage_geofence_ios/pubspec.yaml new file mode 100644 index 00000000..ae52af58 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/pubspec.yaml @@ -0,0 +1,28 @@ +name: moengage_geofence_ios +description: iOS implementation of the moengage_geofence plugin +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_geofence + platforms: + ios: + pluginClass: MoEngageGeofencePlugin + dartPluginClass: MoEngageGeofenceIOS + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.0.0 + moengage_geofence_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/geofence/moengage_geofence_ios/pubspec_overrides.yaml b/flutter-sample/geofence/moengage_geofence_ios/pubspec_overrides.yaml new file mode 100644 index 00000000..8ce913d1 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_ios/pubspec_overrides.yaml @@ -0,0 +1,13 @@ +dependency_overrides: + moengage_geofence_platform_interface: + path: ../moengage_geofence_platform_interface/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/CHANGELOG.md b/flutter-sample/geofence/moengage_geofence_platform_interface/CHANGELOG.md new file mode 100644 index 00000000..8c3c365a --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/CHANGELOG.md @@ -0,0 +1,11 @@ +# MoEngage Geofence Platform Interface + +# 07-12-2023 + +## 1.1.0 +- Updated Minimum Supported `moengage_flutter` version to `6.1.0` + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/LICENSE b/flutter-sample/geofence/moengage_geofence_platform_interface/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/README.md b/flutter-sample/geofence/moengage_geofence_platform_interface/README.md new file mode 100644 index 00000000..1cd5f7ee --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/README.md @@ -0,0 +1,26 @@ +# moengage_geofence_platform_interface + +A common platform interface for the [`moengage_geofence`][1] plugin. + +This interface allows platform-specific implementations of the `moengage_geofence` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `moengage_geofence`, extend +[`MoEngageGeofencePlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`MoEngageGeofencePlatform` by calling +`MoEngageGeofencePlatform.instance = MyPlatformMoEngageGeofence()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../moengage_geofence +[2]: lib/moengage_geofence_platform_interface.dart \ No newline at end of file diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/lib/moengage_geofence_platform_interface.dart b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/moengage_geofence_platform_interface.dart new file mode 100644 index 00000000..6f6a9f73 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/moengage_geofence_platform_interface.dart @@ -0,0 +1,2 @@ +export 'src/internal/moe_geofence_constants.dart'; +export 'src/moengage_geofence_platform_interface.dart'; diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/internal/method_channel_moengage_geofence.dart b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/internal/method_channel_moengage_geofence.dart new file mode 100644 index 00000000..25a2695c --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/internal/method_channel_moengage_geofence.dart @@ -0,0 +1,36 @@ +import 'package:flutter/services.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' + show Logger, getAccountMeta; +import '../../moengage_geofence_platform_interface.dart'; + +/// An implementation of [MoEngageGeofencePlatform] that uses method channels. +class MethodChannelMoEngageGeofence extends MoEngageGeofencePlatform { + /// The method channel used to interact with the native platform. + final MethodChannel _channel = const MethodChannel(geoFenceChannelName); + + final String _tag = '${moduleTagGeofence}MoEAndroidGeofence'; + + @override + void startGeofenceMonitoring(String appId) { + try { + _channel.invokeMethod( + methodStartGeofenceMonitoring, + getAccountMeta(appId), + ); + } catch (e) { + Logger.e('$_tag Error: startGeofenceMonitoring() : $e'); + } + } + + @override + void stopGeofenceMonitoring(String appId) { + try { + _channel.invokeMethod( + methodStopGeofenceMonitoring, + getAccountMeta(appId), + ); + } catch (e) { + Logger.e('$_tag Error: stopGeofenceMonitoring() : $e'); + } + } +} diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/internal/moe_geofence_constants.dart b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/internal/moe_geofence_constants.dart new file mode 100644 index 00000000..67cf3560 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/internal/moe_geofence_constants.dart @@ -0,0 +1,9 @@ +// ignore_for_file: public_member_api_docs +const String moduleTagGeofence = 'Geofence_'; + +// Plugin Channel +const String geoFenceChannelName = 'com.moengage/geofence'; + +// Method names +const String methodStartGeofenceMonitoring = 'startGeofenceMonitoring'; +const String methodStopGeofenceMonitoring = 'stopGeofenceMonitoring'; diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/moengage_geofence_platform_interface.dart b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/moengage_geofence_platform_interface.dart new file mode 100644 index 00000000..8ea9da53 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/lib/src/moengage_geofence_platform_interface.dart @@ -0,0 +1,39 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'internal/method_channel_moengage_geofence.dart'; + +/// The interface that implementations of moengage_geofence must implement. +/// +/// Platform implementations should extend this class +/// rather than implement it as `MoEngageGeofence`. +/// Extending this class (using `extends`) ensures that the subclass will get +/// the default implementation, while platform implementations that `implements` +/// this interface will be broken by newly added [MoEngageGeofencePlatform] methods. +abstract class MoEngageGeofencePlatform extends PlatformInterface { + /// Constructs a MoEngageGeofencePlatform. + MoEngageGeofencePlatform() : super(token: _token); + + static final Object _token = Object(); + + static MoEngageGeofencePlatform _instance = MethodChannelMoEngageGeofence(); + + /// The default instance of [MoEngageGeofencePlatform] to use. + /// + /// Defaults to [MethodChannelMoEngageGeofence]. + static MoEngageGeofencePlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [MoEngageGeofencePlatform] when they register themselves. + static set instance(MoEngageGeofencePlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Starts Geofence Monitoring + /// [appId] - MoEngage App ID + void startGeofenceMonitoring(String appId); + + /// Stops Geofence Monitoring + /// [appId] - MoEngage App ID + void stopGeofenceMonitoring(String appId); +} diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/pubspec.yaml b/flutter-sample/geofence/moengage_geofence_platform_interface/pubspec.yaml new file mode 100644 index 00000000..7a6380aa --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/pubspec.yaml @@ -0,0 +1,19 @@ +name: moengage_geofence_platform_interface +description: A common platform interface for the moengage_geofence plugin. +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.1.0 + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + diff --git a/flutter-sample/geofence/moengage_geofence_platform_interface/pubspec_overrides.yaml b/flutter-sample/geofence/moengage_geofence_platform_interface/pubspec_overrides.yaml new file mode 100644 index 00000000..446c4bf1 --- /dev/null +++ b/flutter-sample/geofence/moengage_geofence_platform_interface/pubspec_overrides.yaml @@ -0,0 +1,11 @@ +dependency_overrides: + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/inbox/.DS_Store b/flutter-sample/inbox/.DS_Store new file mode 100644 index 00000000..1633f4a2 Binary files /dev/null and b/flutter-sample/inbox/.DS_Store differ diff --git a/flutter-sample/inbox/moengage_inbox/CHANGELOG.md b/flutter-sample/inbox/moengage_inbox/CHANGELOG.md new file mode 100644 index 00000000..ec637987 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/CHANGELOG.md @@ -0,0 +1,119 @@ +# MoEngage Flutter Inbox Plugin + +# 07-12-2023 + +## 5.1.0 +- Android + - Native SDK updated to support version `12.10.01` and above. + +# 13-09-2023 + +## 5.0.0 +- Federated Plugin Implementation +- Breaking Change: Use import 'package:moengage_inbox/moengage_inbox.dart'; to import any files in the `moengage_inbox` package. Remove the existing import related to `moengage_inbox` +- Android + - Native SDK updated to support version `12.9.00` and above. + +# 26-07-2023 + +## 4.5.1 +- iOS + - BugFix: Fixed parsing issue in `fetchAllMessages` API. + +# 19-07-2023 + +## 4.5.0 +- iOS + - MoEngageInbox SDK version Updated to `2.10.0` + +# 31-05-2023 + +## 4.4.0 +- Android + - Compile SDK Version Updated to 33 + - Native SDK updated to support version `12.9.00` and above. +- iOS + - MoEngageInbox SDK version updated to `~>2.8.0`. + +# 08-02-2023 +## 4.3.0 +- Security improvement: controlled logging for release, debug and profile mode +- iOS + - MoEngageInbox SDK version updated to `~>2.4.0`. + +# 23-01-2023 +## 4.2.0 +- iOS + - MoEngageInbox SDK version updated to `~>2.2.0`. + +# 27-10-2022 + +## 4.1.0 +- Android + - Android Gradle Plugin version updated to `7.3.1` + - Gradle version updated to `7.4` + - Compile SDK Version - 31 + - Target SDK version - 31 + - Support for Android SDK version `12.4.00` + - Inbox Core `2.2.0` + +## 27.09.2022 + +### 4.0.0 +- Support for Android SDK version `12.2.05` and above and Native Inbox SDK Version `2.1.1` and above. +- Support for iOS SDK version `8.3.1` and above Native Inbox SDK Version `1.3.0` and above. +- Breaking changes + +| Then | Now | +|-----------------|-----------------------------| +| MoEngageInbox() | MoEngageInbox("YOUR_APP_ID) | + +- Android + - Build Configuration Updates + - Minimum SDK version - 21 + - Target SDK version - 30 + - Compile SDK Version - 30 + +### 3.2.0 (29th July 2022) +- Added Flutter 3 support + +### 3.1.0 *(6th September 2021)* +- Android + - Native SDK updated to support version `11.4.00` and above. + +### 3.0.0 *(12th May 2021)* +- Migrated the main library to null safety. + - Require Dart 2.12 or greater. +- Android + - Native SDK updated to support version `11.2.00` and above. + - Removed Native `addon-inbox` SDK support + - Native Inbox artifact changed to support `inbox-core`, version `1.0.00` and above. + +### 2.0.1 *(28th April 2021)* +- iOS + - Podspec changes to set deployment target to iOS 10.0. + +### 2.0.0 *(26th February 2021)* +- iOS + - Native Dependencies updated to support MoEngage-iOS-SDK `7.*` and above + - Base plugin version dependency updated to `~> 2.0.2`. +- Android + - Native SDK updated to support version `11.0.04` and above. + - Native Inbox SDK updated to support version `6.0.2` and above. + - Plugin Base `2.0.00` + +### 1.0.2 *(15th February 2021)* +- Android artifacts use manven central instead of Jcenter. +- Native SDK version `5.3.1` +- Plugin Base `1.0.01` + +### 1.0.1 *(7th November, 2020)* +- Media Content parsing fixes + +### 1.0.0 *(6th November, 2020)* +- Initial release +- APIs + - Fetch All Messages + - Get unclicked count + - Track message clicked + - Delete message diff --git a/flutter-sample/inbox/moengage_inbox/LICENSE b/flutter-sample/inbox/moengage_inbox/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox/README.md b/flutter-sample/inbox/moengage_inbox/README.md new file mode 100644 index 00000000..8e57706d --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/README.md @@ -0,0 +1,37 @@ +# MoEngage Inbox Plugin + +Inbox Plugin for MoEngage Platform + +## SDK Installation + +To add the MoEngage Flutter SDK to your application, edit your application's `pubspec.yaml` file and add the below dependency to it: + +![Download](https://img.shields.io/pub/v/moengage_inbox.svg) + +```yaml +dependencies: + moengage_inbox: $latestSdkVersion +``` +replace `$latestSdkVersion` with the latest SDK version. + +Run flutter packages get to install the SDK. + + Note: This plugin is dependent on `moengage_flutter` plugin. Make sure you have installed the `moengage_flutter + ` make sure you have installed the `moengage_flutter` plugin as well. Refer to the + + ### Android Installation + +![MavenBadge](https://maven-badges.herokuapp.com/maven-central/com.moengage/addon-inbox/badge.svg) + + Once you install the Flutter Plugin add MoEngage's native Android SDK dependency to the Android project of your application. + Navigate to `android --> app --> build.gradle`. Add the MoEngage Android SDK's dependency in the `dependencies` block + + ```groovy + dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation("com.moengage:inbox-core:$sdkVersion") +} + ``` +where `$sdkVersion` should be replaced by the latest version of the MoEngage SDK. + +Refer to the [Documentation](https://developers.moengage.com/hc/en-us/articles/4404365709588-Notification-Center) for complete integration guide. diff --git a/flutter-sample/inbox/moengage_inbox/config.json b/flutter-sample/inbox/moengage_inbox/config.json new file mode 100644 index 00000000..3ec9a805 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/config.json @@ -0,0 +1,3 @@ +{ + "version": "5.1.0" +} \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox/example/README.md b/flutter-sample/inbox/moengage_inbox/example/README.md new file mode 100644 index 00000000..e3de9025 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/example/README.md @@ -0,0 +1,3 @@ +# moengage_inbox_example + +Demonstrates how to use the moengage_inbox plugin. diff --git a/flutter-sample/inbox/moengage_inbox/lib/moengage_inbox.dart b/flutter-sample/inbox/moengage_inbox/lib/moengage_inbox.dart new file mode 100644 index 00000000..deab3bc1 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/lib/moengage_inbox.dart @@ -0,0 +1,2 @@ +export 'package:moengage_inbox_platform_interface/moengage_inbox_platform_interface.dart'; +export 'src/moengage_inbox.dart'; diff --git a/flutter-sample/inbox/moengage_inbox/lib/src/moengage_inbox.dart b/flutter-sample/inbox/moengage_inbox/lib/src/moengage_inbox.dart new file mode 100644 index 00000000..c2e76774 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/lib/src/moengage_inbox.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:moengage_inbox_platform_interface/moengage_inbox_platform_interface.dart'; + +/// Helper Class to interact with MoEngage Cards Feature +class MoEngageInbox { + /// [MoEngageInbox] Constructor + MoEngageInbox(this.appId); + + /// AppId Available in MoEngage Platform + late String appId; + + ///Marks the given message as clicked and tracks a click event for the same. + /// [message] - [InboxMessage] data + void trackMessageClicked(InboxMessage message) { + MoEngageInboxPlatform.instance.trackMessageClicked(message, appId); + } + + /// Deletes the given message from inbox. + /// [message] - [InboxMessage] data + void deleteMessage(InboxMessage message) { + MoEngageInboxPlatform.instance.deleteMessage(message, appId); + } + + /// Gets all the messages saved in the inbox + /// Returns [List] of [InboxData] + Future fetchAllMessages() async { + return MoEngageInboxPlatform.instance.fetchAllMessages(appId); + } + + /// Returns the count of un-clicked messages in the inbox + Future getUnClickedCount() async { + return MoEngageInboxPlatform.instance.getUnClickedCount(appId); + } +} diff --git a/flutter-sample/inbox/moengage_inbox/pubspec.yaml b/flutter-sample/inbox/moengage_inbox/pubspec.yaml new file mode 100644 index 00000000..dd2011c0 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/pubspec.yaml @@ -0,0 +1,32 @@ +name: moengage_inbox +description: Flutter Plugin for using Notification Center feature of MoEngage Platform, to provide an Inbox to view all the notifications received by your application. +version: 5.1.0 +homepage: https://moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.1.0 + moengage_inbox_android: ^1.1.0 + moengage_inbox_ios: ^1.1.0 + moengage_inbox_platform_interface: ^1.1.0 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + +flutter: + assets: + - config.json + plugin: + platforms: + android: + default_package: moengage_inbox_android + ios: + default_package: moengage_inbox_ios \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox/pubspec_overrides.yaml b/flutter-sample/inbox/moengage_inbox/pubspec_overrides.yaml new file mode 100644 index 00000000..bc98ff83 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox/pubspec_overrides.yaml @@ -0,0 +1,17 @@ +dependency_overrides: + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ + moengage_inbox_android: + path: ../moengage_inbox_android/ + moengage_inbox_ios: + path: ../moengage_inbox_ios/ + moengage_inbox_platform_interface: + path: ../moengage_inbox_platform_interface/ diff --git a/flutter-sample/inbox/moengage_inbox_android/CHANGELOG.md b/flutter-sample/inbox/moengage_inbox_android/CHANGELOG.md new file mode 100644 index 00000000..fb38bf76 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/CHANGELOG.md @@ -0,0 +1,13 @@ +# MoEngage Inbox Android Plugin + +# 07-12-2023 + +# 1.1.0 +- Add support for AGP `8.0.2` and above +- Upgrade Kotlin Version to `1.7.10` +- Support for `inbox-core` version `2.6.0` and above + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/LICENSE b/flutter-sample/inbox/moengage_inbox_android/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/README.md b/flutter-sample/inbox/moengage_inbox_android/README.md new file mode 100644 index 00000000..70b967a2 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/README.md @@ -0,0 +1,15 @@ +# moengage\_inbox\_android + +The Android implementation of [`moengage_inbox`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_inbox` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_inbox +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/android/.gitignore b/flutter-sample/inbox/moengage_inbox_android/android/.gitignore new file mode 100644 index 00000000..26659750 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/android/build.gradle b/flutter-sample/inbox/moengage_inbox_android/android/build.gradle new file mode 100644 index 00000000..76a618b4 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/build.gradle @@ -0,0 +1,57 @@ +group 'com.moengage.moengage_inbox' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.0.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdk 33 + namespace "com.moengage.flutter.inbox" + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 21 + } + lintOptions { + disable 'InvalidPackage' + } + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compileOnly("com.moengage:moe-android-sdk:12.10.01") + compileOnly("com.moengage:inbox-core:2.6.0") + implementation("com.moengage:plugin-base-inbox:3.3.0") +} + +apply from: file("./user-agent.gradle") \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/android/gradle.properties b/flutter-sample/inbox/moengage_inbox_android/android/gradle.properties new file mode 100644 index 00000000..87c57042 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/android/gradle/wrapper/gradle-wrapper.properties b/flutter-sample/inbox/moengage_inbox_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a403c412 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/android/settings.gradle b/flutter-sample/inbox/moengage_inbox_android/android/settings.gradle new file mode 100644 index 00000000..d2c09fe9 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'moengage_inbox' diff --git a/flutter-sample/inbox/moengage_inbox_android/android/src/main/AndroidManifest.xml b/flutter-sample/inbox/moengage_inbox_android/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1c156d42 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/flutter-sample/inbox/moengage_inbox_android/android/src/main/kotlin/com/moengage/flutter/inbox/Constants.kt b/flutter-sample/inbox/moengage_inbox_android/android/src/main/kotlin/com/moengage/flutter/inbox/Constants.kt new file mode 100644 index 00000000..59cd8864 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/src/main/kotlin/com/moengage/flutter/inbox/Constants.kt @@ -0,0 +1,18 @@ +package com.moengage.flutter.inbox + +/** + * @author Umang Chamaria + * Date: 2020/10/29 + */ +const val CHANNEL_NAME = "com.moengage/inbox" + +const val METHOD_NAME_UN_CLICKED_COUNT = "unClickedCount"; +const val METHOD_NAME_FETCH_MESSAGES = "fetchMessages"; +const val METHOD_NAME_DELETE_MESSAGE = "deleteMessage"; +const val METHOD_NAME_TRACK_CLICKED = "trackMessageClicked"; + +const val INTEGRATION_TYPE = "flutter" + +// Asset Location for config.json under moengage_flutter package. +const val ASSET_CONFIG_FILE_PATH = "flutter_assets/packages/moengage_inbox/config.json" +const val VERSION_KEY = "version" \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_android/android/src/main/kotlin/com/moengage/flutter/inbox/MoEngageInboxPlugin.kt b/flutter-sample/inbox/moengage_inbox_android/android/src/main/kotlin/com/moengage/flutter/inbox/MoEngageInboxPlugin.kt new file mode 100644 index 00000000..ea7a5282 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/src/main/kotlin/com/moengage/flutter/inbox/MoEngageInboxPlugin.kt @@ -0,0 +1,149 @@ +package com.moengage.flutter.inbox + +import android.content.Context +import android.os.Handler +import android.os.Looper +import androidx.annotation.NonNull +import com.moengage.core.LogLevel +import com.moengage.core.internal.logger.Logger +import com.moengage.plugin.base.inbox.internal.InboxPluginHelper +import com.moengage.plugin.base.inbox.internal.inboxDataToJson +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import org.json.JSONObject +import java.util.concurrent.Executors + +/** MoengageInboxPlugin */ +class MoEngageInboxPlugin : FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + private val tag = "MoEngageInboxPlugin" + private lateinit var context: Context + private val executorService = Executors.newCachedThreadPool() + private val mainThread = Handler(Looper.getMainLooper()) + private val inboxHelper = InboxPluginHelper() + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + inboxHelper.logPluginMeta(INTEGRATION_TYPE, getMoEngageInboxVersion(flutterPluginBinding.applicationContext)) + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + } + + @Suppress("SENSELESS_COMPARISON") + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + try { + if (call == null) { + Logger.print(LogLevel.ERROR) { + "$tag onMethodCall() : MethodCall instance is null " + + "cannot proceed further." + } + return + } + if (context == null) { + Logger.print(LogLevel.ERROR) { + "$tag onMethodCall() : Context is null cannot proceed further." + } + return + } + Logger.print { "$tag onMethodCall() : Method: ${call.method}" } + when (call.method) { + METHOD_NAME_UN_CLICKED_COUNT -> getUnClickedCount(call, result) + METHOD_NAME_FETCH_MESSAGES -> fetchMessages(call, result) + METHOD_NAME_DELETE_MESSAGE -> deleteMessage(call, result) + METHOD_NAME_TRACK_CLICKED -> trackMessageClicked(call, result) + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag onMethodCall() : " } + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + private fun getUnClickedCount(c: MethodCall, result: Result) { + try { + if (c.arguments == null) return + val payload: String = c.arguments.toString() + Logger.print { "$tag getUnClickedCount() : Will fetch unclicked count" } + executorService.submit { + val countJson = inboxHelper.getUnClickedMessagesCount(context, payload) + Logger.print { "$tag getUnClickedCount() : Count: $countJson" } + mainThread.post { + try { + result.success(countJson) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getUnClickedCount() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getUnClickedCount() : " } + } + } + + private fun fetchMessages(call: MethodCall, result: Result) { + try { + if (call.arguments == null) return + val payload: String = call.arguments.toString() + val inboxData = inboxHelper.fetchAllMessages(context, payload) ?: return + executorService.submit { + val serialisedMessages = inboxDataToJson(inboxData) + mainThread.post { + try { + Logger.print { "$tag fetchMessages() : serialisedMessages: $serialisedMessages" } + result.success(serialisedMessages.toString()) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag fetchMessages() : " } + } + } + } + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag fetchMessages() : " } + } + } + + private fun deleteMessage(call: MethodCall, result: Result) { + try { + if (call.arguments == null) return + val payload = call.arguments.toString() + Logger.print { "$tag deleteMessage() : Argument :$payload" } + inboxHelper.deleteMessage(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag deleteMessage() : " } + } + } + + private fun trackMessageClicked(call: MethodCall, result: Result) { + try { + if (call.arguments == null) return + val payload = call.arguments.toString() + Logger.print { "$tag trackMessageClicked() : Argument :$payload" } + inboxHelper.trackMessageClicked(context, payload) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag trackMessageClicked() : " } + } + } + + /** + * Get moengage_inbox version from Config File + */ + private fun getMoEngageInboxVersion(context: Context): String { + return try { + val json = context.assets.open(ASSET_CONFIG_FILE_PATH) + .bufferedReader().use { it.readText() } + JSONObject(json).getString(VERSION_KEY) + } catch (t: Throwable) { + Logger.print(LogLevel.ERROR, t) { "$tag getMoEngageFlutterVersion() : " } + "" + } + } +} diff --git a/flutter-sample/inbox/moengage_inbox_android/android/user-agent.gradle b/flutter-sample/inbox/moengage_inbox_android/android/user-agent.gradle new file mode 100644 index 00000000..4425e180 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/android/user-agent.gradle @@ -0,0 +1,19 @@ +import java.util.regex.Matcher +import java.util.regex.Pattern + +String libraryVersionName = "UNKNOWN" +File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml') + +if (pubspec.exists()) { + String yaml = pubspec.text + // Using \s*['|"]?([^\n|'|"]*)['|"]? to extract version number. + Matcher versionMatcher = Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", Pattern.MULTILINE).matcher(yaml) + if (versionMatcher.find()) libraryVersionName = versionMatcher.group(1).replaceAll("\\+", "-") +} + +android { + defaultConfig { + // BuildConfig.VERSION_NAME + buildConfigField("String", 'MOENGAGE_INBOX_FLUTTER_LIBRARY_VERSION', "\"${libraryVersionName}\"") + } +} diff --git a/flutter-sample/inbox/moengage_inbox_android/lib/moengage_inbox_android.dart b/flutter-sample/inbox/moengage_inbox_android/lib/moengage_inbox_android.dart new file mode 100644 index 00000000..f3414241 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/lib/moengage_inbox_android.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; +import 'dart:core'; + +import 'package:flutter/services.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' + show getAccountMeta, Logger; +import 'package:moengage_inbox_platform_interface/moengage_inbox_platform_interface.dart'; + +/// The Android implementation of [MoEngageInboxPlatform]. +class MoEngageInboxAndroid extends MoEngageInboxPlatform { + final MethodChannel _channel = const MethodChannel(CHANNEL_NAME); + + /// Registers this class as the default instance of [MoEngageInboxPlatform] + static void registerWith() { + Logger.v('Registering MoEngageInboxAndroid with Platform Interface'); + MoEngageInboxPlatform.instance = MoEngageInboxAndroid(); + } + + @override + Future getUnClickedCount(String appId) async { + final Map payload = getAccountMeta(appId); + final dynamic unClickedCountPayload = await _channel.invokeMethod( + METHOD_NAME_UN_CLICKED_COUNT, json.encode(payload)); + return fetchUnclickedCount(unClickedCountPayload); + } + + @override + void trackMessageClicked(InboxMessage message, String appId) { + final Map payload = getImpressionPayload(message, appId); + _channel.invokeMethod(METHOD_NAME_TRACK_CLICKED, json.encode(payload)); + } + + @override + void deleteMessage(InboxMessage message, String appId) { + final Map payload = getImpressionPayload(message, appId); + _channel.invokeMethod(METHOD_NAME_DELETE_MESSAGE, json.encode(payload)); + } + + @override + Future fetchAllMessages(String appId) async { + final Map payload = getAccountMeta(appId); + final serialisedMessages = await _channel.invokeMethod( + METHOD_NAME_FETCH_MESSAGES, json.encode(payload)); + return deSerializeInboxMessages(serialisedMessages.toString()); + } +} diff --git a/flutter-sample/inbox/moengage_inbox_android/pubspec.yaml b/flutter-sample/inbox/moengage_inbox_android/pubspec.yaml new file mode 100644 index 00000000..8ba174f1 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/pubspec.yaml @@ -0,0 +1,29 @@ +name: moengage_inbox_android +description: Android implementation of the moengage_inbox plugin +version: 1.1.0 +homepage: https://moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_inbox + platforms: + android: + package: com.moengage.flutter.inbox + pluginClass: MoEngageInboxPlugin + dartPluginClass: MoEngageInboxAndroid + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.1.0 + moengage_inbox_platform_interface: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/inbox/moengage_inbox_android/pubspec_overrides.yaml b/flutter-sample/inbox/moengage_inbox_android/pubspec_overrides.yaml new file mode 100644 index 00000000..ca92fac9 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/pubspec_overrides.yaml @@ -0,0 +1,13 @@ +dependency_overrides: + moengage_inbox_platform_interface: + path: ../moengage_inbox_platform_interface/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ diff --git a/flutter-sample/inbox/moengage_inbox_android/test/moengage_inbox_android_test.dart b/flutter-sample/inbox/moengage_inbox_android/test/moengage_inbox_android_test.dart new file mode 100644 index 00000000..1f508431 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_android/test/moengage_inbox_android_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); +} diff --git a/flutter-sample/inbox/moengage_inbox_ios/CHANGELOG.md b/flutter-sample/inbox/moengage_inbox_ios/CHANGELOG.md new file mode 100644 index 00000000..39fa54e5 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/CHANGELOG.md @@ -0,0 +1,11 @@ +# MoEngage Inbox iOS Plugin + +# 01-12-2023 + +## 1.1.0 +- Updated MoEngageInbox to 2.13.0 + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_ios/LICENSE b/flutter-sample/inbox/moengage_inbox_ios/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_ios/README.md b/flutter-sample/inbox/moengage_inbox_ios/README.md new file mode 100644 index 00000000..e19b53ad --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/README.md @@ -0,0 +1,15 @@ +# moengage\_inbox\_ios + +The iOS implementation of [`moengage_inbox`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `moengage_inbox` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/moengage_inbox +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_ios/ios/.gitignore b/flutter-sample/inbox/moengage_inbox_ios/ios/.gitignore new file mode 100644 index 00000000..aa479fd3 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/ios/.gitignore @@ -0,0 +1,37 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_ios/ios/Assets/.gitkeep b/flutter-sample/inbox/moengage_inbox_ios/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageFlutterInbox.swift b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageFlutterInbox.swift new file mode 100644 index 00000000..8d5ead69 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageFlutterInbox.swift @@ -0,0 +1,49 @@ +import Flutter +import UIKit +import MoEngagePluginBase +import MoEngagePluginInbox + +public class MoEngageFlutterInbox: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: MoEngageFlutterInboxConstants.kPluginChannelName, binaryMessenger: registrar.messenger()) + let instance = MoEngageFlutterInbox() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let payload = call.arguments as? [String: Any] else{ return } + + switch call.method { + case MoEngageFlutterInboxConstants.MethodNames.kFetchMessages: + MoEngagePluginInboxBridge.sharedInstance.getInboxMessages(payload) { messagesPayload in + DispatchQueue.main.async { + if let jsonData = try? JSONSerialization.data(withJSONObject: messagesPayload), + let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue) { + result(jsonString) + return + } + } + } + + case MoEngageFlutterInboxConstants.MethodNames.kGetUnclickedCount: + MoEngagePluginInboxBridge.sharedInstance.getUnreadMessageCount(payload) { unreadCountPayload in + DispatchQueue.main.async { + if let jsonData = try? JSONSerialization.data(withJSONObject: unreadCountPayload), + let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue) { + result(jsonString) + return + } + } + } + + case MoEngageFlutterInboxConstants.MethodNames.kTrackMessageClicked: + MoEngagePluginInboxBridge.sharedInstance.trackInboxClick(payload) + + case MoEngageFlutterInboxConstants.MethodNames.kDeleteMessage: + MoEngagePluginInboxBridge.sharedInstance.deleteInboxEntry(payload) + + default: + print("Invalid invocation: \(call.method)") + } + } +} diff --git a/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageFlutterInboxConstants.swift b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageFlutterInboxConstants.swift new file mode 100644 index 00000000..45eccc19 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageFlutterInboxConstants.swift @@ -0,0 +1,22 @@ +// +// MoEngageFlutterInboxConstants.swift +// moengage_inbox +// +// Created by Chengappa C D on 06/11/20. +// + +import Foundation + + +struct MoEngageFlutterInboxConstants{ + + static let kPluginChannelName = "com.moengage/inbox" + + struct MethodNames { + static let kFetchMessages = "fetchMessages" + static let kTrackMessageClicked = "trackMessageClicked" + static let kDeleteMessage = "deleteMessage" + static let kGetUnclickedCount = "unClickedCount" + } + +} diff --git a/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageInboxPlugin.h b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageInboxPlugin.h new file mode 100644 index 00000000..f2afc4d8 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageInboxPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface MoEngageInboxPlugin : NSObject +@end diff --git a/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageInboxPlugin.m b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageInboxPlugin.m new file mode 100644 index 00000000..4fbc6182 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/ios/Classes/MoEngageInboxPlugin.m @@ -0,0 +1,15 @@ +#import "MoEngageInboxPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "moengage_inbox_ios-Swift.h" +#endif + +@implementation MoEngageInboxPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [MoEngageFlutterInbox registerWithRegistrar:registrar]; +} +@end diff --git a/flutter-sample/inbox/moengage_inbox_ios/ios/moengage_inbox_ios.podspec b/flutter-sample/inbox/moengage_inbox_ios/ios/moengage_inbox_ios.podspec new file mode 100644 index 00000000..8b680e1b --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/ios/moengage_inbox_ios.podspec @@ -0,0 +1,22 @@ +require 'yaml' +pubspec = YAML.load_file(File.join('..', 'pubspec.yaml')) +libraryVersion = pubspec['version'].gsub('+', '-') + +Pod::Spec.new do |s| + s.name = 'moengage_inbox_ios' + s.version = libraryVersion + s.platform = :ios + s.ios.deployment_target = '11.0' + s.summary = 'A flutter plugin for using Notification Inbox from MoEngage iOS and Android SDKs.' + s.description = <<-DESC +A flutter plugin for using Notification Inbox from MoEngage iOS and Android SDKs. + DESC + s.homepage = 'https://www.moengage.com/' + s.license = { :file => '../LICENSE' } + s.author = { 'MoEngage Inc.' => 'mobiledevs@moengage.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'MoEngagePluginInbox', '~> 2.5.1' + s.swift_version = '5.0' +end \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_ios/lib/moengage_inbox_ios.dart b/flutter-sample/inbox/moengage_inbox_ios/lib/moengage_inbox_ios.dart new file mode 100644 index 00000000..8388b927 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/lib/moengage_inbox_ios.dart @@ -0,0 +1,47 @@ +import 'dart:core'; + +import 'package:flutter/services.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' + show getAccountMeta, Logger; +import 'package:moengage_inbox_platform_interface/moengage_inbox_platform_interface.dart'; + +/// IOS implementation of [MoEngageInboxPlatform]. +class MoEngageInboxIOS extends MoEngageInboxPlatform { + /// [MoEngageInboxIOS] Constructor + MoEngageInboxIOS(); + final MethodChannel _channel = const MethodChannel(CHANNEL_NAME); + + /// Registers this class as the default instance of [MoEngageInboxPlatform] + static void registerWith() { + Logger.v('Registering MoEngageInboxIOS with Platform Interface'); + MoEngageInboxPlatform.instance = MoEngageInboxIOS(); + } + + @override + Future getUnClickedCount(String appId) async { + final Map payload = getAccountMeta(appId); + final unClickedCountPayload = + await _channel.invokeMethod(METHOD_NAME_UN_CLICKED_COUNT, payload); + return fetchUnclickedCount(unClickedCountPayload); + } + + @override + void trackMessageClicked(InboxMessage message, String appId) { + final Map payload = getImpressionPayload(message, appId); + _channel.invokeMethod(METHOD_NAME_TRACK_CLICKED, payload); + } + + @override + void deleteMessage(InboxMessage message, String appId) { + final Map payload = getImpressionPayload(message, appId); + _channel.invokeMethod(METHOD_NAME_DELETE_MESSAGE, payload); + } + + @override + Future fetchAllMessages(String appId) async { + final Map payload = getAccountMeta(appId); + final serialisedMessages = + await _channel.invokeMethod(METHOD_NAME_FETCH_MESSAGES, payload); + return deSerializeInboxMessages(serialisedMessages); + } +} diff --git a/flutter-sample/inbox/moengage_inbox_ios/pubspec.yaml b/flutter-sample/inbox/moengage_inbox_ios/pubspec.yaml new file mode 100644 index 00000000..ed70e20b --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/pubspec.yaml @@ -0,0 +1,28 @@ +name: moengage_inbox_ios +description: iOS implementation of the moengage_inbox plugin +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: moengage_inbox + platforms: + ios: + pluginClass: MoEngageInboxPlugin + dartPluginClass: MoEngageInboxIOS + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.0.0 + moengage_inbox_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + diff --git a/flutter-sample/inbox/moengage_inbox_ios/pubspec_overrides.yaml b/flutter-sample/inbox/moengage_inbox_ios/pubspec_overrides.yaml new file mode 100644 index 00000000..ea1b518a --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/pubspec_overrides.yaml @@ -0,0 +1,13 @@ +dependency_overrides: + moengage_inbox_platform_interface: + path: ../moengage_inbox_platform_interface/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_ios/test/moengage_inbox_ios_test.dart b/flutter-sample/inbox/moengage_inbox_ios/test/moengage_inbox_ios_test.dart new file mode 100644 index 00000000..1f508431 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_ios/test/moengage_inbox_ios_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/CHANGELOG.md b/flutter-sample/inbox/moengage_inbox_platform_interface/CHANGELOG.md new file mode 100644 index 00000000..41fdbf8b --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/CHANGELOG.md @@ -0,0 +1,11 @@ +# MoEngage Inbox Platform Interface + +# 07-12-2023 + +## 1.1.0 +- Updated Minimum Supported `moengage_flutter` version to `6.1.0` + +# 13-09-2023 + +## 1.0.0 +- Initial Release \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/LICENSE b/flutter-sample/inbox/moengage_inbox_platform_interface/LICENSE new file mode 100644 index 00000000..59fe62ec --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2014-2023 MoEngage Inc. +All rights reserved. +* Use of source code or binaries contained within MoEngage SDK is permitted only to enable use of the MoEngage platform by customers of MoEngage. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of MoEngage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/README.md b/flutter-sample/inbox/moengage_inbox_platform_interface/README.md new file mode 100644 index 00000000..b9f25bd5 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/README.md @@ -0,0 +1,26 @@ +# moengage_inbox_platform_interface + +A common platform interface for the [`moengage_inbox`][1] plugin. + +This interface allows platform-specific implementations of the `moengage_inbox` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `moengage_inbox`, extend +[`MoEngageInboxPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`MoEngageInboxPlatform` by calling +`MoEngageInboxPlatform.instance = MyPlatformMoEngageInbox()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../moengage_inbox +[2]: lib/moengage_inbox_platform_interface.dart \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/moengage_inbox_platform_interface.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/moengage_inbox_platform_interface.dart new file mode 100644 index 00000000..88e84617 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/moengage_inbox_platform_interface.dart @@ -0,0 +1,5 @@ +export 'src/internal/constants.dart'; +export 'src/internal/moe_inbox_utils.dart'; +export 'src/model/models.dart'; +export 'src/moengage_inbox_platform_interface.dart'; +export 'src/payload_transformer.dart'; diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/internal/constants.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/internal/constants.dart new file mode 100644 index 00000000..91f6c50d --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/internal/constants.dart @@ -0,0 +1,39 @@ +// ignore_for_file: public_member_api_docs +const String TAG = 'Inbox_'; +const String CHANNEL_NAME = 'com.moengage/inbox'; + +const String METHOD_NAME_UN_CLICKED_COUNT = 'unClickedCount'; +const String METHOD_NAME_FETCH_MESSAGES = 'fetchMessages'; +const String METHOD_NAME_DELETE_MESSAGE = 'deleteMessage'; +const String METHOD_NAME_TRACK_CLICKED = 'trackMessageClicked'; + +// Payload keys +const String keyUnClickedCount = 'unClickedCount'; + +const String keyPlatform = 'platform'; +const String keyMessages = 'messages'; + +const String keyId = 'id'; +const String keyCampaignId = 'campaignId'; +const String keyIsClicked = 'isClicked'; +const String keyReceivedTime = 'receivedTime'; +const String keyExpiryTime = 'expiry'; +const String keyPayload = 'payload'; +const String keyTag = 'tag'; + +const String keyTextContent = 'text'; +const String keyTextContentTitle = 'title'; +const String keyTextContentMessage = 'message'; +const String keyTextContentSummary = 'summary'; +const String keyTextContentSubTitle = 'subtitle'; + +const String keyMediaContent = 'media'; + +const String keyType = 'type'; +const String keyUrl = 'url'; + +const String keyAction = 'action'; +const String keyActionType = 'actionType'; +const String keyNavigationType = 'navigationType'; +const String keyValue = 'value'; +const String keyKvPair = 'kvPair'; diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/internal/moe_inbox_utils.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/internal/moe_inbox_utils.dart new file mode 100644 index 00000000..2ae15fe7 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/internal/moe_inbox_utils.dart @@ -0,0 +1,14 @@ +import 'package:moengage_flutter/moengage_flutter.dart' + show keyAccountMeta, keyAppId, keyData; +import '../model/inbox_message.dart'; +import '../payload_transformer.dart'; + +/// Get Impression Payload [Map] +Map getImpressionPayload( + InboxMessage inboxMessage, String appId) { + final dynamic inboxPayload = messageToMap(inboxMessage); + return { + keyAccountMeta: {keyAppId: appId}, + keyData: inboxPayload + }; +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/method_channel_moengage_inbox.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/method_channel_moengage_inbox.dart new file mode 100644 index 00000000..1d907625 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/method_channel_moengage_inbox.dart @@ -0,0 +1,42 @@ +import 'package:flutter/services.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' show getAccountMeta; + +import './internal/constants.dart'; +import './internal/moe_inbox_utils.dart'; +import 'model/models.dart'; +import 'moengage_inbox_platform_interface.dart'; +import 'payload_transformer.dart'; + +/// An implementation of [MoEngageInboxPlatform] that uses method channels. +class MethodChannelMoEngageInbox extends MoEngageInboxPlatform { + /// The method channel used to interact with the native platform. + final MethodChannel _channel = const MethodChannel(CHANNEL_NAME); + + @override + Future getUnClickedCount(String appId) async { + final Map payload = getAccountMeta(appId); + final unClickedCountPayload = + await _channel.invokeMethod(METHOD_NAME_UN_CLICKED_COUNT, payload); + return fetchUnclickedCount(unClickedCountPayload); + } + + @override + void trackMessageClicked(InboxMessage message, String appId) { + final Map payload = getImpressionPayload(message, appId); + _channel.invokeMethod(METHOD_NAME_TRACK_CLICKED, payload); + } + + @override + void deleteMessage(InboxMessage message, String appId) { + final Map payload = getImpressionPayload(message, appId); + _channel.invokeMethod(METHOD_NAME_DELETE_MESSAGE, payload); + } + + @override + Future fetchAllMessages(String appId) async { + final Map payload = getAccountMeta(appId); + final serialisedMessages = + await _channel.invokeMethod(METHOD_NAME_FETCH_MESSAGES, payload); + return deSerializeInboxMessages(serialisedMessages); + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/action.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/action.dart new file mode 100644 index 00000000..567b512a --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/action.dart @@ -0,0 +1,15 @@ +import '../../moengage_inbox_platform_interface.dart'; + +/// Action Data for [InboxMessage] +class Action { + /// [Action] Constructor + Action(this.actionType); + + /// Type of Action + ActionType actionType; + + @override + String toString() { + return 'Action{actionType: $actionType}'; + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/action_type.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/action_type.dart new file mode 100644 index 00000000..6e4ca448 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/action_type.dart @@ -0,0 +1,27 @@ +/// Action Type for [InboxMessage] +enum ActionType { + /// Navigation Action + navigation +} + +/// Extension functions for [ActionType] +extension ActionTypeExt on ActionType { + /// Convert [ActionType] to [String] + String get asString { + switch (this) { + case ActionType.navigation: + return _valueNavigation; + } + } + + /// Get [ActionType] from [String] + static ActionType fromString(String string) { + switch (string) { + case _valueNavigation: + return ActionType.navigation; + } + throw Exception('unsupported type'); + } +} + +const String _valueNavigation = 'navigation'; diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/inbox_data.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/inbox_data.dart new file mode 100644 index 00000000..0979f8bb --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/inbox_data.dart @@ -0,0 +1,18 @@ +import 'inbox_message.dart'; + +/// Inbox Messages Data +class InboxData { + /// [InboxData] Constructor + InboxData(this.platform, this.messages); + + /// Native platform from which the callback was triggered. + String platform; + + /// List of [InboxMessage] + List messages; + + @override + String toString() { + return 'InboxData{platform: $platform, messages: $messages}'; + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/inbox_message.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/inbox_message.dart new file mode 100644 index 00000000..f72276ee --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/inbox_message.dart @@ -0,0 +1,58 @@ +import 'action.dart'; +import 'media.dart'; +import 'text_content.dart'; + +/// Data object for Inbox Messages +class InboxMessage { + /// [InboxMessage] Constructor + InboxMessage( + this.id, + this.campaignId, + this.textContent, + this.isClicked, + this.media, + this.action, + this.tag, + this.receivedTime, + this.expiry, + this.payload); + + /// internal identifier used by the SDK for storage. + int id; + + /// Unique identifier for a message. + String campaignId; + + /// Text content of the message. Instance of [TextContent] + TextContent textContent; + + /// true if the message has been clicked by the user else false + bool isClicked; + + /// Media content associated with the message. + Media? media; + + /// List of actions to be executed on click. + List action; + + /// Tag associated to the message. + String tag; + + /// The time in which the message was received on the device. + /// + /// Format - ISO-8601 yyyy-MM-dd'T'HH:mm:ss'Z' + String receivedTime; + + /// The time at which the message expiry. + /// + /// Format - ISO-8601 yyyy-MM-dd'T'HH:mm:ss'Z' + String expiry; + + /// Complete message payload. + Map payload; + + @override + String toString() { + return 'InboxMessage{id: $id, campaignId: $campaignId, textContent: $textContent, isClicked: $isClicked, media: $media, action: $action, tag: $tag, receivedTime: $receivedTime, expiry: $expiry, payload: $payload}'; + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/media.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/media.dart new file mode 100644 index 00000000..fe18acc8 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/media.dart @@ -0,0 +1,18 @@ +import 'media_type.dart'; + +/// Media associated with the [InboxMessage] +class Media { + /// [Media] constructor + Media(this.mediaType, this.url); + + /// Content type of the Media. + MediaType mediaType; + + /// Url for the media content. Generally a http(s) url. + String url; + + @override + String toString() { + return 'Media{mediaType: $mediaType, url: $url}'; + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/media_type.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/media_type.dart new file mode 100644 index 00000000..96571a33 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/media_type.dart @@ -0,0 +1,43 @@ +/// Possible media types for [InboxMessage] +enum MediaType { + /// Image Type + image, + + /// Audio Type + audio, + + /// Video Type + video +} + +/// Extension Util For [MediaType] +extension MediaTypeExt on MediaType { + /// Convert [MediaType] to [String] + String get asString { + switch (this) { + case MediaType.image: + return _valueImage; + case MediaType.audio: + return _valueAudio; + case MediaType.video: + return _valueVideo; + } + } + + /// Get [MediaType] Instance from [String] + static MediaType fromString(String string) { + switch (string) { + case _valueImage: + return MediaType.image; + case _valueAudio: + return MediaType.audio; + case _valueVideo: + return MediaType.video; + } + throw Exception('unsupported type'); + } +} + +const String _valueAudio = 'audio'; +const String _valueImage = 'image'; +const String _valueVideo = 'video'; diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/models.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/models.dart new file mode 100644 index 00000000..9949824a --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/models.dart @@ -0,0 +1,9 @@ +export 'action.dart'; +export 'action_type.dart'; +export 'inbox_data.dart'; +export 'inbox_message.dart'; +export 'media.dart'; +export 'media_type.dart'; +export 'navigation_action.dart'; +export 'navigation_type.dart'; +export 'text_content.dart'; diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/navigation_action.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/navigation_action.dart new file mode 100644 index 00000000..0fd82fc4 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/navigation_action.dart @@ -0,0 +1,23 @@ +import 'action.dart'; +import 'navigation_type.dart'; + +/// Navigation Action +class NavigationAction extends Action { + /// [NavigationAction] Constructor + NavigationAction( + super.actionType, this.navigationType, this.value, this.kvPair); + + /// Navigation Type + NavigationType navigationType; + + /// Url or screen name to navigate to + String value; + + /// Additional Key value pair associated with action + Map kvPair; + + @override + String toString() { + return 'NavigationAction{navigationType: $navigationType, value: $value, kvPair: $kvPair}'; + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/navigation_type.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/navigation_type.dart new file mode 100644 index 00000000..45deac5c --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/navigation_type.dart @@ -0,0 +1,43 @@ +/// Navigation Type for Inbox Action +enum NavigationType { + /// DeepLinking Action + deepLink, + + /// RichLanding Action + richLanding, + + /// Screen Name Action + screenName +} + +/// Extension for [NavigationType] +extension NavigationTypeExt on NavigationType { + /// Convert [NavigationType] to String + String get asString { + switch (this) { + case NavigationType.deepLink: + return _valueDeepLink; + case NavigationType.screenName: + return _valueScreenName; + case NavigationType.richLanding: + return _valueRichLanding; + } + } + + /// Get [NavigationType] From String + static NavigationType fromString(String string) { + switch (string) { + case _valueRichLanding: + return NavigationType.richLanding; + case _valueDeepLink: + return NavigationType.deepLink; + case _valueScreenName: + return NavigationType.screenName; + } + throw Exception('unsupported type'); + } +} + +const String _valueDeepLink = 'deepLink'; +const String _valueRichLanding = 'richLanding'; +const String _valueScreenName = 'screenName'; diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/text_content.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/text_content.dart new file mode 100644 index 00000000..98a6c98f --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/model/text_content.dart @@ -0,0 +1,26 @@ +/// Text Content for [InboxMessage] +class TextContent { + /// [InboxMessage] Constructor + TextContent(this.title, this.message, this.summary, this.subtitle); + + /// Title string for the inbox message. + String title; + + /// Message string for the inbox message. + String message; + + /// Summary string for the inbox message. + /// + /// Note: This is present for Android Platform. + String summary; + + /// Subtitle string for the inbox message. + /// + /// Note: This is present only for the iOS Platform. + String subtitle; + + @override + String toString() { + return 'title: $title, message: $message, summary: $summary, subtitle: $subtitle'; + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/moengage_inbox_platform_interface.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/moengage_inbox_platform_interface.dart new file mode 100644 index 00000000..4986198a --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/moengage_inbox_platform_interface.dart @@ -0,0 +1,45 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'method_channel_moengage_inbox.dart'; +import 'model/inbox_data.dart'; +import 'model/inbox_message.dart'; + +/// Platform Interface for MoEngage Inbox Feature +abstract class MoEngageInboxPlatform extends PlatformInterface { + /// Constructs a MoEngageInboxPlatform. + MoEngageInboxPlatform() : super(token: _token); + + static final Object _token = Object(); + + static MoEngageInboxPlatform _instance = MethodChannelMoEngageInbox(); + + /// The default instance of [MoEngageInboxPlatform] to use. + /// + /// Defaults to [MethodChannelMoEngageInbox]. + static MoEngageInboxPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [MoEngageInboxPlatform] when they register themselves. + static set instance(MoEngageInboxPlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Returns the count of un-clicked messages in the inbox + /// [appId] - MoEngage App ID + Future getUnClickedCount(String appId); + + ///Marks the given message as clicked and tracks a click event for the same. + /// [message] - MoEngage App ID + /// [appId] - MoEngage App ID + void trackMessageClicked(InboxMessage message, String appId); + + /// Deletes the given message from inbox. + /// [message] - [InboxMessage] + /// [appId] - MoEngage App ID + void deleteMessage(InboxMessage message, String appId); + + /// Gets all the messages saved in the inbox + /// [appId] - MoEngage App ID + Future fetchAllMessages(String appId); +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/payload_transformer.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/payload_transformer.dart new file mode 100644 index 00000000..8e7d0827 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/lib/src/payload_transformer.dart @@ -0,0 +1,198 @@ +import 'dart:convert'; + +import 'package:moengage_flutter/moengage_flutter.dart' show Logger, keyData; +import 'internal/constants.dart'; +import 'model/models.dart'; + +const String _tag = '${TAG}PayloadTransformer'; + +/// UnClicked Messages Count +int fetchUnclickedCount(dynamic unClickedPayload) { + final payload = json.decode(unClickedPayload.toString()); + final dataPayload = payload[keyData]; + if (dataPayload.isNotEmpty == true) { + final int unclickedCount = + (dataPayload.containsKey(keyUnClickedCount) == true + ? dataPayload[keyUnClickedCount] + : 0) as int; + return unclickedCount; + } + + return 0; +} + +/// Convert Message To Map +Map messageToMap(InboxMessage inboxMessage) { + final Map message = { + keyId: inboxMessage.id, + keyCampaignId: inboxMessage.campaignId, + keyIsClicked: inboxMessage.isClicked, + keyReceivedTime: inboxMessage.receivedTime, + keyExpiryTime: inboxMessage.expiry, + keyPayload: inboxMessage.payload, + keyTextContent: mapFromTextContent(inboxMessage.textContent), + keyAction: actionsListFromModel(inboxMessage.action) + }; + final Media? media = inboxMessage.media; + if (media != null) { + message[keyMediaContent] = mapFromMedia(media); + } + return message; +} + +/// Parse [InboxData] from JSON Strig +InboxData? deSerializeInboxMessages(dynamic messagesPayload) { + try { + final message = json.decode(messagesPayload.toString()); + final dataPayload = message[keyData]; + return InboxData(dataPayload[keyPlatform].toString(), + messagesJsonToList(dataPayload[keyMessages] as List)); + } catch (e, stackTrace) { + Logger.e('$_tag Error: ', error: e, stackTrace: stackTrace); + } + return null; +} + +/// Get [List] of [InboxMessage] from JSON Array +List messagesJsonToList(List messageArray) { + final List messages = []; + for (final dynamic message in messageArray) { + final InboxMessage? inboxMessage = + messageFromJson(message as Map); + if (inboxMessage != null) { + messages.add(inboxMessage); + } + } + return messages; +} + +/// Get [InboxMessage] from JSON +InboxMessage? messageFromJson(Map message) { + try { + return InboxMessage( + (message.containsKey(keyId) ? message[keyId] : -1) as int, + message[keyCampaignId].toString(), + textContentFromMap(message[keyTextContent] as Map), + (message[keyIsClicked] ?? false) as bool, + message.containsKey(keyMediaContent) + ? mediaFromMap(message[keyMediaContent] as Map) + : null, + actionsFromMap(message[keyAction] as List), + (message.containsKey(keyTag) ? message[keyTag] : 'general').toString(), + message[keyReceivedTime].toString(), + message[keyExpiryTime].toString(), + message[keyPayload] as Map); + } catch (e, stacktrace) { + Logger.e('$_tag Error: messageFromJson InboxMessage ', + stackTrace: stacktrace); + } + return null; +} + +/// Get [TextContent] from [Map] +TextContent textContentFromMap(Map textMap) { + return TextContent( + (textMap.containsKey(keyTextContentTitle) + ? textMap[keyTextContentTitle] + : '') + .toString(), + textMap[keyTextContentMessage].toString(), + (textMap.containsKey(keyTextContentSummary) + ? textMap[keyTextContentSummary] + : '') + .toString(), + (textMap.containsKey(keyTextContentSubTitle) + ? textMap[keyTextContentSubTitle] + : '') + .toString()); +} + +/// Get [Media] from [Map] +Media? mediaFromMap(Map mediaMap) { + if (mediaMap.isEmpty) { + return null; + } + return Media(MediaTypeExt.fromString(mediaMap[keyType].toString()), + mediaMap[keyUrl].toString()); +} + +/// Get [List] of [Action] from Json Array +List actionsFromMap(List actions) { + final List actionList = []; + for (final action in actions) { + final Action? parsedAction = actionFromMap(action as Map); + if (parsedAction != null) { + actionList.add(parsedAction); + } + } + return actionList; +} + +/// Get [Action] from [Map] +Action? actionFromMap(Map actionMap) { + final ActionType actionType = + ActionTypeExt.fromString(actionMap[keyActionType].toString()); + switch (actionType) { + case ActionType.navigation: + return navigationActionFromMap(actionType, actionMap); + } +} + +/// Get [NavigationAction] from [Map] +NavigationAction navigationActionFromMap( + ActionType actionType, Map navigationMap) { + return NavigationAction( + actionType, + NavigationTypeExt.fromString(navigationMap[keyNavigationType].toString()), + navigationMap[keyValue].toString(), + navigationMap[keyKvPair] as Map); +} + +/// Get [Map] from [TextContent] +Map mapFromTextContent(TextContent content) { + return { + keyTextContentTitle: content.title, + keyTextContentMessage: content.message, + keyTextContentSummary: content.summary, + keyTextContentSubTitle: content.subtitle + }; +} + +/// Get [Map] from [Media] +Map mapFromMedia(Media media) { + return {keyType: media.mediaType.asString, keyUrl: media.url}; +} + +/// Get [List] of [Map] from [List] of [Action] +List> actionsListFromModel(List actions) { + final List> actionsList = >[]; + for (final Action action in actions) { + final Map? actionMap = actionToMap(action); + if (actionMap != null) { + actionsList.add(actionMap); + } + } + return actionsList; +} + +/// Convert [Action] to [Map] +Map? actionToMap(Action action) { + switch (action.actionType) { + case ActionType.navigation: + return navigationActionToMap(action as NavigationAction); + // ignore: no_default_cases + default: + return null; + } +} + +/// Convert [NavigationAction] to [Map] +Map navigationActionToMap(NavigationAction navigationAction) { + final Map navigationMap = { + keyActionType: navigationAction.actionType.asString, + keyNavigationType: navigationAction.navigationType.asString, + keyValue: navigationAction.value, + keyKvPair: navigationAction.kvPair + }; + return navigationMap; +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/pubspec.yaml b/flutter-sample/inbox/moengage_inbox_platform_interface/pubspec.yaml new file mode 100644 index 00000000..599b42b7 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/pubspec.yaml @@ -0,0 +1,20 @@ +name: moengage_inbox_platform_interface +description: A common platform interface for the moengage_inbox plugin. +version: 1.1.0 +homepage: https://www.moengage.com + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + moengage_flutter: ^6.1.0 + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + collection: ^1.16.0 + flutter_test: + sdk: flutter + diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/pubspec_overrides.yaml b/flutter-sample/inbox/moengage_inbox_platform_interface/pubspec_overrides.yaml new file mode 100644 index 00000000..446c4bf1 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/pubspec_overrides.yaml @@ -0,0 +1,11 @@ +dependency_overrides: + moengage_flutter: + path: ../../core/moengage_flutter/ + moengage_flutter_platform_interface: + path: ../../core/moengage_flutter_platform_interface/ + moengage_flutter_android: + path: ../../core/moengage_flutter_android/ + moengage_flutter_ios: + path: ../../core/moengage_flutter_ios/ + moengage_flutter_web: + path: ../../core/moengage_flutter_web/ \ No newline at end of file diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/test/inbox_comparator.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/test/inbox_comparator.dart new file mode 100644 index 00000000..fdb7467b --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/test/inbox_comparator.dart @@ -0,0 +1,69 @@ +import 'package:collection/collection.dart'; +import 'package:moengage_flutter/moengage_flutter.dart' show AccountMeta; +import 'package:moengage_inbox_platform_interface/moengage_inbox_platform_interface.dart'; + +class InboxComparator { + bool isInboxDataEqual(InboxData? data1, InboxData? data2) { + return data1?.platform == data2?.platform && + isInboxMessagesEqual(data1?.messages ?? [], + data2?.messages ?? []); + } + + bool isAccountMetaEqual(AccountMeta? data1, AccountMeta? data2) { + return data1?.appId == data2?.appId; + } + + bool isInboxMessageEqual(InboxMessage data1, InboxMessage data2) { + return data1.isClicked == data1.isClicked && + data1.campaignId == data2.campaignId && + const DeepCollectionEquality().equals(data1.payload, data2.payload) && + data1.expiry == data2.expiry && + data1.id == data2.id && + isInboxActionsEqual(data1.action, data2.action) && + data1.campaignId == data2.campaignId && + isMediaEqual(data1.media, data2.media) && + data1.receivedTime == data2.receivedTime && + data1.tag == data2.tag && + isTextContentEqual(data1.textContent, data2.textContent); + } + + bool isInboxMessagesEqual( + List data1, List data2) { + if (data1.length != data2.length) { + return false; + } + for (int i = 0; i < data1.length; i++) { + if (!isInboxMessageEqual(data1[i], data2[i])) { + return false; + } + } + return true; + } + + bool isMediaEqual(Media? data1, Media? data2) { + return data1?.mediaType == data2?.mediaType && data1?.url == data2?.url; + } + + bool isTextContentEqual(TextContent? data1, TextContent? data2) { + return data1?.title == data2?.title && + data1?.subtitle == data2?.subtitle && + data1?.message == data2?.message && + data1?.summary == data2?.summary; + } + + bool isInboxActionsEqual(List data1, List data2) { + if (data1.length != data2.length) { + return false; + } + for (int i = 0; i < data1.length; i++) { + if (!isActionEqual(data1[i], data2[i])) { + return false; + } + } + return true; + } + + bool isActionEqual(Action? action1, Action? action2) { + return action1?.actionType == action2?.actionType; + } +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/test/parser_test.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/test/parser_test.dart new file mode 100644 index 00000000..3c6c4dea --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/test/parser_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:moengage_inbox_platform_interface/moengage_inbox_platform_interface.dart'; + +import 'inbox_comparator.dart'; +import 'src/data_provider_provider.dart'; +import 'src/json_data_provider.dart'; + +void main() { + test('Test Inbox Data', () { + expect( + InboxComparator().isInboxDataEqual( + deSerializeInboxMessages(inboxPayload), inboxData), + true); + }); + + test('Test Inbox Click Parser', () { + expect(fetchUnclickedCount(inboxClickData), 1); + }); +} diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/test/src/data_provider_provider.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/test/src/data_provider_provider.dart new file mode 100644 index 00000000..023411a8 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/test/src/data_provider_provider.dart @@ -0,0 +1,54 @@ +import 'package:moengage_inbox_platform_interface/moengage_inbox_platform_interface.dart'; + +final InboxData inboxData = InboxData('android', [ + InboxMessage( + 1, + '000000000000000088470679_L_0', + TextContent('Title', 'Message', 'Summary', ''), + false, + Media(MediaType.image, 'https://picsum.photos/200/222'), + [ + NavigationAction(ActionType.navigation, NavigationType.deepLink, + 'monengage://moe_app/add_to_cart?key=value', { + 'gcm_image_url': 'https://picsum.photos/200/222', + 'gcm_alert': 'Message', + 'gcm_notificationType': 'gcm_webNotification', + 'push_from': 'moengage', + 'gcm_webUrl': 'monengage://moe_app/add_to_cart', + 'moe_app_id': 'DAO6UGZ73D9RTK8B5W96TPYN_DEBUG', + 'gcm_campaign_id': '000000000000000088470679_L_0', + 'moe_channel_id': 'moe_sound_channel', + 'moe_webUrl': 'monengage://moe_app/add_to_cart?key=value', + 'gcm_subtext': 'Summary', + 'moe_cid_attr': + "{'moe_campaign_channel': 'Push', 'moe_delivery_type': 'One Time', 'moe_campaign_id': '000000000000000088470679'}", + 'mi_image_url': 'https://picsum.photos/200/222', + 'MOE_MSG_RECEIVED_TIME': 1692080250167, + 'key': 'value', + 'gcm_title': 'Title', + 'moe_notification_posted_time': 1692080250175 + }) + ], + 'general', + '2023-08-15T06:17:30.167Z', + '2023-11-13T06:17:30.000Z', + { + 'moe_app_id': 'DAO6UGZ73D9RTK8B5W96TPYN_DEBUG', + 'moe_notification_posted_time': 1692080250175, + 'gcm_subtext': 'Summary', + 'moe_webUrl': 'monengage://moe_app/add_to_cart?key=value', + 'gcm_notificationType': 'gcm_webNotification', + 'gcm_image_url': 'https://picsum.photos/200/222', + 'moe_cid_attr': + "{'moe_campaign_channel': 'Push', 'moe_delivery_type': 'One Time', 'moe_campaign_id': '000000000000000088470679'}", + 'mi_image_url': 'https://picsum.photos/200/222', + 'push_from': 'moengage', + 'MOE_MSG_RECEIVED_TIME': 1692080250167, + 'key': 'value', + 'gcm_alert': 'Message', + 'gcm_title': 'Title', + 'gcm_webUrl': 'monengage://moe_app/add_to_cart', + 'gcm_campaign_id': '000000000000000088470679_L_0', + 'moe_channel_id': 'moe_sound_channel' + }), +]); diff --git a/flutter-sample/inbox/moengage_inbox_platform_interface/test/src/json_data_provider.dart b/flutter-sample/inbox/moengage_inbox_platform_interface/test/src/json_data_provider.dart new file mode 100644 index 00000000..03e01ec6 --- /dev/null +++ b/flutter-sample/inbox/moengage_inbox_platform_interface/test/src/json_data_provider.dart @@ -0,0 +1,82 @@ +const String inboxPayload = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "messages": [ + { + "id": 1, + "campaignId": "000000000000000088470679_L_0", + "text": { + "title": "Title", + "message": "Message", + "summary": "Summary" + }, + "action": [ + { + "actionType": "navigation", + "navigationType": "deepLink", + "value": "monengage://moe_app/add_to_cart?key=value", + "kvPair": { + "gcm_image_url": "https://picsum.photos/200/222", + "gcm_alert": "Message", + "gcm_notificationType": "gcm_webNotification", + "push_from": "moengage", + "gcm_webUrl": "monengage://moe_app/add_to_cart", + "moe_app_id": "DAO6UGZ73D9RTK8B5W96TPYN_DEBUG", + "gcm_campaign_id": "000000000000000088470679_L_0", + "moe_channel_id": "moe_sound_channel", + "moe_webUrl": "monengage://moe_app/add_to_cart?key=value", + "gcm_subtext": "Summary", + "moe_cid_attr": "{'moe_campaign_channel': 'Push', 'moe_delivery_type': 'One Time', 'moe_campaign_id': '000000000000000088470679'}", + "mi_image_url": "https://picsum.photos/200/222", + "MOE_MSG_RECEIVED_TIME": 1692080250167, + "key": "value", + "gcm_title": "Title", + "moe_notification_posted_time": 1692080250175 + } + } + ], + "isClicked": false, + "receivedTime": "2023-08-15T06:17:30.167Z", + "expiry": "2023-11-13T06:17:30.000Z", + "tag": "general", + "payload": { + "moe_app_id": "DAO6UGZ73D9RTK8B5W96TPYN_DEBUG", + "moe_notification_posted_time": 1692080250175, + "gcm_subtext": "Summary", + "moe_webUrl": "monengage://moe_app/add_to_cart?key=value", + "gcm_notificationType": "gcm_webNotification", + "gcm_image_url": "https://picsum.photos/200/222", + "moe_cid_attr": "{'moe_campaign_channel': 'Push', 'moe_delivery_type': 'One Time', 'moe_campaign_id': '000000000000000088470679'}", + "mi_image_url": "https://picsum.photos/200/222", + "push_from": "moengage", + "MOE_MSG_RECEIVED_TIME": 1692080250167, + "key": "value", + "gcm_alert": "Message", + "gcm_title": "Title", + "gcm_webUrl": "monengage://moe_app/add_to_cart", + "gcm_campaign_id": "000000000000000088470679_L_0", + "moe_channel_id": "moe_sound_channel" + }, + "media": { + "type": "image", + "url": "https://picsum.photos/200/222" + } + } + ], + "platform": "android" + } +} +'''; + +const String inboxClickData = ''' +{ + "accountMeta": { + "appId": "" + }, + "data": { + "unClickedCount": 1 + } +}'''; diff --git a/flutter-sample/scripts/cards/.DS_Store b/flutter-sample/scripts/cards/.DS_Store new file mode 100644 index 00000000..590a3a4e Binary files /dev/null and b/flutter-sample/scripts/cards/.DS_Store differ diff --git a/flutter-sample/scripts/core/.DS_Store b/flutter-sample/scripts/core/.DS_Store new file mode 100644 index 00000000..ceca3504 Binary files /dev/null and b/flutter-sample/scripts/core/.DS_Store differ diff --git a/flutter-sample/scripts/core/moengage_flutter_web/.DS_Store b/flutter-sample/scripts/core/moengage_flutter_web/.DS_Store new file mode 100644 index 00000000..b4549d4e Binary files /dev/null and b/flutter-sample/scripts/core/moengage_flutter_web/.DS_Store differ diff --git a/flutter-sample/scripts/example/.DS_Store b/flutter-sample/scripts/example/.DS_Store new file mode 100644 index 00000000..99a266f6 Binary files /dev/null and b/flutter-sample/scripts/example/.DS_Store differ diff --git a/flutter-sample/scripts/geofence/.DS_Store b/flutter-sample/scripts/geofence/.DS_Store new file mode 100644 index 00000000..5335ed88 Binary files /dev/null and b/flutter-sample/scripts/geofence/.DS_Store differ diff --git a/flutter-sample/scripts/inbox/.DS_Store b/flutter-sample/scripts/inbox/.DS_Store new file mode 100644 index 00000000..1633f4a2 Binary files /dev/null and b/flutter-sample/scripts/inbox/.DS_Store differ diff --git a/flutter-sample/scripts/setup.sh b/flutter-sample/scripts/setup.sh new file mode 100755 index 00000000..b88dcba3 --- /dev/null +++ b/flutter-sample/scripts/setup.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Replace 'YOUR_FLUTTER_PROJECTS_PATH' with the path to your main folder containing all Flutter projects +PROJECTS_PATH=".." +# Find all directories that contain a pubspec.yaml file (assuming these are Flutter projects) +FLUTTER_PROJECT_DIRS=$(find "$PROJECTS_PATH" -name "pubspec.yaml" -exec dirname {} \;) +# Iterate through each project directory and run 'flutter pub get' +for dir in $FLUTTER_PROJECT_DIRS; do + echo "Running 'flutter pub get' in $dir" + pushd "$dir" > /dev/null + flutter clean + flutter pub get + popd > /dev/null +done +echo "All Flutter projects updated."