From 9cb3fabdf1f5fa10d65770a181ced937413642a2 Mon Sep 17 00:00:00 2001 From: Jagandeep Brar Date: Wed, 1 Jun 2022 21:36:43 -0400 Subject: [PATCH] refactor: refactored in-app purchase interface --- lib/core.dart | 1 - lib/core/models/logs/log_type.dart | 35 +++---- lib/core/models/types/links.dart | 54 +++------- lib/core/models/types/list_view_option.dart | 32 +++--- lib/core/system/in_app_purchases.dart | 96 ------------------ lib/database/tables/radarr.dart | 2 +- lib/database/tables/sonarr.dart | 2 +- lib/main.dart | 3 +- lib/modules/settings/core/router.dart | 3 +- .../settings/routes/donations/route.dart | 99 ++++++------------- .../settings/routes/notifications/route.dart | 2 +- .../settings/routes/settings/route.dart | 3 +- .../routes/system_logs/pages/log_details.dart | 2 +- .../filesystem/platform/filesystem_stub.dart | 1 - lib/system/flavor.dart | 62 +++++------- .../in_app_purchase/in_app_purchase.dart | 18 ++++ .../platform/in_app_purchase_html.dart | 6 ++ .../platform/in_app_purchase_io.dart | 85 ++++++++++++++++ .../platform/in_app_purchase_stub.dart | 6 ++ .../in_app_purchase/purchase_option.dart | 90 +++++++++++++++++ localization/lunasea/en.json | 1 + 21 files changed, 317 insertions(+), 286 deletions(-) delete mode 100644 lib/core/system/in_app_purchases.dart create mode 100644 lib/system/in_app_purchase/in_app_purchase.dart create mode 100644 lib/system/in_app_purchase/platform/in_app_purchase_html.dart create mode 100644 lib/system/in_app_purchase/platform/in_app_purchase_io.dart create mode 100644 lib/system/in_app_purchase/platform/in_app_purchase_stub.dart create mode 100644 lib/system/in_app_purchase/purchase_option.dart diff --git a/lib/core.dart b/lib/core.dart index 644551b1..77059d93 100644 --- a/lib/core.dart +++ b/lib/core.dart @@ -20,7 +20,6 @@ export 'core/router/router.dart'; export 'core/services/the_movie_db.dart'; export 'core/state/module_state.dart'; export 'core/state/state.dart'; -export 'core/system/in_app_purchases.dart'; export 'core/system/profile.dart'; export 'core/utilities/changelog.dart'; export 'core/utilities/dialogs.dart'; diff --git a/lib/core/models/logs/log_type.dart b/lib/core/models/logs/log_type.dart index 3c4aec6e..6313eaf2 100644 --- a/lib/core/models/logs/log_type.dart +++ b/lib/core/models/logs/log_type.dart @@ -4,20 +4,26 @@ import 'package:lunasea/system/flavor.dart'; part 'log_type.g.dart'; +const TYPE_DEBUG = 'debug'; +const TYPE_WARNING = 'warning'; +const TYPE_ERROR = 'error'; +const TYPE_CRITICAL = 'critical'; + @HiveType(typeId: 24, adapterName: 'LunaLogTypeAdapter') enum LunaLogType { @HiveField(0) - WARNING, + WARNING(TYPE_WARNING), @HiveField(1) - ERROR, + ERROR(TYPE_ERROR), @HiveField(2) - CRITICAL, + CRITICAL(TYPE_CRITICAL), @HiveField(3) - DEBUG, -} + DEBUG(TYPE_DEBUG); -extension LunaLogTypeExtension on LunaLogType { - LunaLogType? fromKey(String key) { + final String key; + const LunaLogType(this.key); + + static LunaLogType? fromKey(String key) { switch (key) { case 'warning': return LunaLogType.WARNING; @@ -30,20 +36,9 @@ extension LunaLogTypeExtension on LunaLogType { } return null; } +} - String get key { - switch (this) { - case LunaLogType.WARNING: - return 'warning'; - case LunaLogType.ERROR: - return 'error'; - case LunaLogType.CRITICAL: - return 'critical'; - case LunaLogType.DEBUG: - return 'debug'; - } - } - +extension LunaLogTypeExtension on LunaLogType { bool get enabled { switch (this) { case LunaLogType.WARNING: diff --git a/lib/core/models/types/links.dart b/lib/core/models/types/links.dart index 2038e7a1..7d3ada60 100644 --- a/lib/core/models/types/links.dart +++ b/lib/core/models/types/links.dart @@ -2,46 +2,20 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/extensions/string_links.dart'; enum LunaLinks { - BUILDS, - CHANGELOG, - DISCORD, - DOCUMENTATION, - FEEDBACK_BOARD, - GITHUB, - NOTIFICATIONS_GETTING_STARTED, - REDDIT, - BUILD_CHANNELS, - WEBLATE, - WEBSITE, -} + CHANGELOG('https://www.lunasea.app/changelog'), + BUILD_CHANNELS('https://www.lunasea.app/build-channels'), + BUILDS('https://www.lunasea.app/builds'), + DISCORD('https://www.lunasea.app/discord'), + DOCUMENTATION('https://www.lunasea.app/docs'), + FEEDBACK_BOARD('https://www.lunasea.app/feedback'), + GITHUB('https://www.lunasea.app/github'), + NOTIFICATIONS_DOC('https://docs.lunasea.app/lunasea/notifications'), + REDDIT('https://www.lunasea.app/reddit'), + WEBLATE('https:/www.lunasea.app/translate'), + WEBSITE('https://www.lunasea.app'); -extension LunaLinksExtension on LunaLinks { - String get url { - switch (this) { - case LunaLinks.BUILDS: - return 'https://www.lunasea.app/builds'; - case LunaLinks.CHANGELOG: - return 'https://www.lunasea.app/changelog'; - case LunaLinks.DISCORD: - return 'https://www.lunasea.app/discord'; - case LunaLinks.DOCUMENTATION: - return 'https://www.lunasea.app/docs'; - case LunaLinks.FEEDBACK_BOARD: - return 'https://www.lunasea.app/feedback'; - case LunaLinks.GITHUB: - return 'https://www.lunasea.app/github'; - case LunaLinks.REDDIT: - return 'https://www.lunasea.app/reddit'; - case LunaLinks.BUILD_CHANNELS: - return 'https://www.lunasea.app/build-channels'; - case LunaLinks.WEBLATE: - return 'https:/www.lunasea.app/translate'; - case LunaLinks.WEBSITE: - return 'https://www.lunasea.app'; - case LunaLinks.NOTIFICATIONS_GETTING_STARTED: - return 'https://docs.lunasea.app/lunasea/notifications'; - } - } + final String url; + const LunaLinks(this.url); - Future launch() async => this.url.openLink(); + Future launch() async => url.openLink(); } diff --git a/lib/core/models/types/list_view_option.dart b/lib/core/models/types/list_view_option.dart index 07f5a355..b825e5e2 100644 --- a/lib/core/models/types/list_view_option.dart +++ b/lib/core/models/types/list_view_option.dart @@ -3,17 +3,31 @@ import 'package:lunasea/core.dart'; part 'list_view_option.g.dart'; +const _BLOCK_VIEW = 'BLOCK_VIEW'; +const _GRID_VIEW = 'GRID_VIEW'; + @HiveType(typeId: 29, adapterName: 'LunaListViewOptionAdapter') enum LunaListViewOption { @HiveField(0) - BLOCK_VIEW, + BLOCK_VIEW(_BLOCK_VIEW), @HiveField(1) - GRID_VIEW, + GRID_VIEW(_GRID_VIEW); + + final String key; + const LunaListViewOption(this.key); + + static LunaListViewOption? fromKey(String? key) { + switch (key) { + case _BLOCK_VIEW: + return LunaListViewOption.BLOCK_VIEW; + case _GRID_VIEW: + return LunaListViewOption.GRID_VIEW; + } + return null; + } } extension LunaListViewOptionExtension on LunaListViewOption { - String get key => this.name; - String get readable { switch (this) { case LunaListViewOption.BLOCK_VIEW: @@ -31,14 +45,4 @@ extension LunaListViewOptionExtension on LunaListViewOption { return Icons.grid_view_rounded; } } - - LunaListViewOption? fromKey(String? key) { - switch (key) { - case 'BLOCK_VIEW': - return LunaListViewOption.BLOCK_VIEW; - case 'GRID_VIEW': - return LunaListViewOption.GRID_VIEW; - } - return null; - } } diff --git a/lib/core/system/in_app_purchases.dart b/lib/core/system/in_app_purchases.dart deleted file mode 100644 index d4d91c5f..00000000 --- a/lib/core/system/in_app_purchases.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:in_app_purchase/in_app_purchase.dart'; -import 'package:lunasea/core.dart'; -import 'package:lunasea/system/platform.dart'; - -extension ProductDetailsExtension on ProductDetails { - IconData get icon { - switch (this.id) { - case LunaInAppPurchases.DONATION_01: - return Icons.local_drink_rounded; - case LunaInAppPurchases.DONATION_03: - return Icons.local_cafe_rounded; - case LunaInAppPurchases.DONATION_05: - return Icons.local_bar_rounded; - case LunaInAppPurchases.DONATION_10: - return Icons.fastfood_rounded; - default: - return Icons.attach_money_rounded; - } - } - - String get name { - switch (this.id) { - case LunaInAppPurchases.DONATION_01: - return 'lunasea.BuyMeASoda'.tr(); - case LunaInAppPurchases.DONATION_03: - return 'lunasea.BuyMeACoffee'.tr(); - case LunaInAppPurchases.DONATION_05: - return 'lunasea.BuyMeABeer'.tr(); - case LunaInAppPurchases.DONATION_10: - return 'lunasea.BuyMeABurger'.tr(); - default: - return 'lunasea.Unknown'.tr(); - } - } -} - -class LunaInAppPurchases { - // Donation IAP IDs - static const DONATION_01 = 'donation_01'; - static const DONATION_03 = 'donation_03'; - static const DONATION_05 = 'donation_05'; - static const DONATION_10 = 'donation_10'; - static const DONATION_IDS = [ - DONATION_01, - DONATION_03, - DONATION_05, - DONATION_10 - ]; - - static InAppPurchase get connection => InAppPurchase.instance; - static StreamSubscription>? _purchaseStream; - static List donationIAPs = []; - static bool isAvailable = false; - - static bool get isSupported => LunaPlatform.isMobile; - - /// Callback function for [purchaseStream]. - static Future _purchasedCallback( - List purchases) async { - for (var purchase in purchases) { - /// Handle donations by always completing the purchase - if (DONATION_IDS.contains(purchase.productID)) { - if (purchase.pendingCompletePurchase) - await connection.completePurchase(purchase); - } - } - } - - /// Initialize the in-app purchases connection. - /// - /// - Enables pending purchase - /// - Attach [InAppPurchaseConnection.instance.purchaseUpdateStream] listener for automatically completing IAP purchases - /// - Fetch and store donation IAPs - Future initialize() async { - if (isSupported) { - isAvailable = await connection.isAvailable(); - if (isAvailable) { - // Open listener - _purchaseStream = connection.purchaseStream.listen( - LunaInAppPurchases._purchasedCallback, - ); - // Load donation IAPs - ProductDetailsResponse donation = - await connection.queryProductDetails(Set.from(DONATION_IDS)); - donationIAPs = donation.productDetails; - donationIAPs.sort((a, b) => a.id.compareTo(b.id)); - } - } - } - - /// Deinitialize the in-app purchases connection. - /// - /// - Cleanly cancel/close the [InAppPurchaseConnection.instance.purchaseUpdateStream] listener. - Future? deinitialize() async => _purchaseStream?.cancel(); -} diff --git a/lib/database/tables/radarr.dart b/lib/database/tables/radarr.dart index 50368f94..f60b25c7 100644 --- a/lib/database/tables/radarr.dart +++ b/lib/database/tables/radarr.dart @@ -101,7 +101,7 @@ enum RadarrDatabase with LunaTableMixin { return; } if (this == RadarrDatabase.DEFAULT_VIEW_MOVIES) { - final item = LunaListViewOption.GRID_VIEW.fromKey(value.toString()); + final item = LunaListViewOption.fromKey(value.toString()); if (item != null) update(item as T); return; } diff --git a/lib/database/tables/sonarr.dart b/lib/database/tables/sonarr.dart index a0e822d8..29d61848 100644 --- a/lib/database/tables/sonarr.dart +++ b/lib/database/tables/sonarr.dart @@ -103,7 +103,7 @@ enum SonarrDatabase with LunaTableMixin { return; } if (this == SonarrDatabase.DEFAULT_VIEW_SERIES) { - final item = LunaListViewOption.GRID_VIEW.fromKey(value.toString()); + final item = LunaListViewOption.fromKey(value.toString()); if (item != null) update(item as T); return; } diff --git a/lib/main.dart b/lib/main.dart index 0d8a5a2e..be08e668 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:lunasea/firebase/firestore.dart'; import 'package:lunasea/firebase/messaging.dart'; import 'package:lunasea/modules/dashboard/routes/dashboard/route.dart' show HomeRouter; +import 'package:lunasea/system/in_app_purchase/in_app_purchase.dart'; import 'package:lunasea/system/localization.dart'; import 'package:lunasea/system/network/network.dart'; import 'package:lunasea/system/quick_actions/quick_actions.dart'; @@ -33,7 +34,7 @@ Future main() async { if (LunaNetwork.isSupported) LunaNetwork().initialize(); if (LunaImageCache.isSupported) LunaImageCache().initialize(); LunaRouter().initialize(); - await LunaInAppPurchases().initialize(); + if (LunaInAppPurchase.isSupported) LunaInAppPurchase().initialize(); await LunaLocalization().initialize(); // Run application return runApp(const LunaBIOS()); diff --git a/lib/modules/settings/core/router.dart b/lib/modules/settings/core/router.dart index d2fe88aa..75dc9f00 100644 --- a/lib/modules/settings/core/router.dart +++ b/lib/modules/settings/core/router.dart @@ -3,6 +3,7 @@ import 'package:lunasea/core.dart'; import 'package:lunasea/firebase/core.dart'; import 'package:lunasea/firebase/messaging.dart'; import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/system/in_app_purchase/in_app_purchase.dart'; class SettingsRouter extends LunaModuleRouter { @override @@ -18,7 +19,7 @@ class SettingsRouter extends LunaModuleRouter { SettingsNotificationsRouter().defineRoute(router); } // Donations - if (LunaInAppPurchases.isSupported) { + if (LunaInAppPurchase.isSupported) { SettingsDonationsRouter().defineRoute(router); SettingsDonationsThankYouRouter().defineRoute(router); } diff --git a/lib/modules/settings/routes/donations/route.dart b/lib/modules/settings/routes/donations/route.dart index cb7d9b38..b8d74d04 100644 --- a/lib/modules/settings/routes/donations/route.dart +++ b/lib/modules/settings/routes/donations/route.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:lunasea/core.dart'; import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/system/in_app_purchase/purchase_option.dart'; +import 'package:lunasea/system/in_app_purchase/in_app_purchase.dart'; class SettingsDonationsRouter extends SettingsPageRouter { SettingsDonationsRouter() : super('/settings/donations'); @@ -20,48 +21,8 @@ class _Widget extends StatefulWidget { } class _State extends State<_Widget> with LunaScrollControllerMixin { - final GlobalKey _scaffoldKey = GlobalKey(); - static StreamSubscription>? purchaseStream; - - @override - void initState() { - super.initState(); - if (LunaInAppPurchases.isAvailable) { - purchaseStream = LunaInAppPurchases.connection.purchaseStream - .listen(_purchasedCallback); - } - } - - @override - void dispose() { - purchaseStream?.cancel(); - super.dispose(); - } - - Future _purchasedCallback(List purchases) async { - for (var purchase in purchases) { - if (purchase.pendingCompletePurchase) { - switch (purchase.status) { - case PurchaseStatus.error: - _purchaseFailed(); - break; - case PurchaseStatus.purchased: - _purchasedSuccess(); - break; - default: - break; - } - } - } - } - - void _purchasedSuccess() => - SettingsDonationsThankYouRouter().navigateTo(context); - - void _purchaseFailed() => showLunaErrorSnackBar( - title: 'Transaction Failure', - message: 'The transaction has failed, please try again', - ); + final _scaffoldKey = GlobalKey(); + final _donationOptions = LunaInAppPurchase().getOptions(); @override Widget build(BuildContext context) { @@ -80,35 +41,31 @@ class _State extends State<_Widget> with LunaScrollControllerMixin { } Widget _body() { - if (!LunaInAppPurchases.isAvailable || - LunaInAppPurchases.donationIAPs.isEmpty) { - return LunaMessage.goBack( - context: context, - text: 'Not Available', - ); - } - return LunaListViewBuilder( - controller: scrollController, - itemCount: LunaInAppPurchases.donationIAPs.length, - itemBuilder: (context, index) => - _iapTile(LunaInAppPurchases.donationIAPs[index]), - ); - } - - Widget _iapTile(ProductDetails product) { - return LunaBlock( - title: product.name, - body: [TextSpan(text: product.price)], - trailing: LunaIconButton(icon: product.icon), - onTap: () async { - final PurchaseParam _parameters = PurchaseParam( - productDetails: product, - ); - await LunaInAppPurchases.connection.buyConsumable( - purchaseParam: _parameters, - autoConsume: true, - ); + return FutureBuilder( + future: _donationOptions, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return LunaListViewBuilder( + controller: scrollController, + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => _tile(snapshot.data![index]), + ); + } else { + return LunaMessage.goBack( + context: context, + text: 'Not Available', + ); + } }, ); } + + Widget _tile(PurchaseOption option) { + return LunaBlock( + title: option.title, + body: [TextSpan(text: option.product!.price)], + trailing: LunaIconButton(icon: option.icon), + onTap: () async => LunaInAppPurchase().purchaseOption(option), + ); + } } diff --git a/lib/modules/settings/routes/notifications/route.dart b/lib/modules/settings/routes/notifications/route.dart index 0e17464a..7d5c4cb3 100644 --- a/lib/modules/settings/routes/notifications/route.dart +++ b/lib/modules/settings/routes/notifications/route.dart @@ -61,7 +61,7 @@ class _State extends State<_Widget> with LunaScrollControllerMixin { title: 'Getting Started', body: const [TextSpan(text: 'Information & Setup Instructions')], trailing: const LunaIconButton.arrow(), - onTap: LunaLinks.NOTIFICATIONS_GETTING_STARTED.launch, + onTap: LunaLinks.NOTIFICATIONS_DOC.launch, ), _enableInAppNotifications(), LunaDivider(), diff --git a/lib/modules/settings/routes/settings/route.dart b/lib/modules/settings/routes/settings/route.dart index c1972979..a45f8c4a 100644 --- a/lib/modules/settings/routes/settings/route.dart +++ b/lib/modules/settings/routes/settings/route.dart @@ -4,6 +4,7 @@ import 'package:lunasea/firebase/core.dart'; import 'package:lunasea/firebase/messaging.dart'; import 'package:lunasea/modules/settings.dart'; import 'package:lunasea/system/flavor.dart'; +import 'package:lunasea/system/in_app_purchase/in_app_purchase.dart'; class SettingsHomeRouter extends SettingsPageRouter { SettingsHomeRouter() : super('/settings'); @@ -76,7 +77,7 @@ class _State extends State<_Widget> with LunaScrollControllerMixin { onTap: () async => SettingsProfilesRouter().navigateTo(context), ), LunaDivider(), - if (LunaInAppPurchases.isSupported) + if (LunaInAppPurchase.isSupported) LunaBlock( title: 'settings.Donations'.tr(), body: [TextSpan(text: 'settings.DonationsDescription'.tr())], diff --git a/lib/modules/settings/routes/system_logs/pages/log_details.dart b/lib/modules/settings/routes/system_logs/pages/log_details.dart index 3862ed2e..bdd2eae6 100644 --- a/lib/modules/settings/routes/system_logs/pages/log_details.dart +++ b/lib/modules/settings/routes/system_logs/pages/log_details.dart @@ -28,7 +28,7 @@ class SettingsSystemLogsDetailsRouter extends SettingsPageRouter { (context, params) { String type = (params['type']?.isNotEmpty ?? false) ? params['type']![0] : ''; - return _Widget(type: LunaLogType.ERROR.fromKey(type)); + return _Widget(type: LunaLogType.fromKey(type)); }, ); } diff --git a/lib/system/filesystem/platform/filesystem_stub.dart b/lib/system/filesystem/platform/filesystem_stub.dart index f1817203..88a95e94 100644 --- a/lib/system/filesystem/platform/filesystem_stub.dart +++ b/lib/system/filesystem/platform/filesystem_stub.dart @@ -1,5 +1,4 @@ // ignore: always_use_package_imports -// ignore: always_use_package_imports import '../filesystem.dart'; bool isPlatformSupported() => false; diff --git a/lib/system/flavor.dart b/lib/system/flavor.dart index 79124c68..558a2c0f 100644 --- a/lib/system/flavor.dart +++ b/lib/system/flavor.dart @@ -2,18 +2,35 @@ import 'package:flutter/material.dart'; import 'package:lunasea/system/environment.dart'; import 'package:lunasea/widgets/ui.dart'; -const _EDGE = 'edge'; -const _BETA = 'beta'; -const _CANDIDATE = 'candidate'; -const _STABLE = 'stable'; +const FLAVOR_EDGE = 'edge'; +const FLAVOR_BETA = 'beta'; +const FLAVOR_CANDIDATE = 'candidate'; +const FLAVOR_STABLE = 'stable'; enum LunaFlavor { - EDGE, - BETA, - CANDIDATE, - STABLE; + EDGE(FLAVOR_EDGE), + BETA(FLAVOR_BETA), + CANDIDATE(FLAVOR_CANDIDATE), + STABLE(FLAVOR_STABLE); - static LunaFlavor get current => LunaFlavor.EDGE.from(LunaEnvironment.flavor); + final String key; + const LunaFlavor(this.key); + + static LunaFlavor fromKey(String key) { + switch (key) { + case FLAVOR_EDGE: + return LunaFlavor.EDGE; + case FLAVOR_BETA: + return LunaFlavor.BETA; + case FLAVOR_CANDIDATE: + return LunaFlavor.CANDIDATE; + case FLAVOR_STABLE: + return LunaFlavor.STABLE; + } + throw Exception('Invalid LunaFlavor'); + } + + static LunaFlavor get current => LunaFlavor.fromKey(LunaEnvironment.flavor); } extension LunaFlavorExtension on LunaFlavor { @@ -49,19 +66,6 @@ extension LunaFlavorExtension on LunaFlavor { } } - String get key { - switch (this) { - case LunaFlavor.EDGE: - return _EDGE; - case LunaFlavor.BETA: - return _BETA; - case LunaFlavor.CANDIDATE: - return _CANDIDATE; - case LunaFlavor.STABLE: - return _STABLE; - } - } - String get name { switch (this) { case LunaFlavor.EDGE: @@ -87,18 +91,4 @@ extension LunaFlavorExtension on LunaFlavor { return LunaColours.accent; } } - - LunaFlavor from(String key) { - switch (key) { - case _EDGE: - return LunaFlavor.EDGE; - case _BETA: - return LunaFlavor.BETA; - case _CANDIDATE: - return LunaFlavor.CANDIDATE; - case _STABLE: - return LunaFlavor.STABLE; - } - throw Exception('Invalid LunaFlavor'); - } } diff --git a/lib/system/in_app_purchase/in_app_purchase.dart b/lib/system/in_app_purchase/in_app_purchase.dart new file mode 100644 index 00000000..14080b7c --- /dev/null +++ b/lib/system/in_app_purchase/in_app_purchase.dart @@ -0,0 +1,18 @@ +// ignore: always_use_package_imports +import 'package:lunasea/system/in_app_purchase/purchase_option.dart'; + +// ignore: always_use_package_imports +import 'platform/in_app_purchase_stub.dart' + if (dart.library.io) 'platform/in_app_purchase_io.dart' + if (dart.library.html) 'platform/in_app_purchase_html.dart'; + +abstract class LunaInAppPurchase { + static bool get isSupported => isPlatformSupported(); + factory LunaInAppPurchase() => getInAppPurchase(); + + Future initialize(); + Future deinitialize(); + Future isAvailable(); + Future> getOptions(); + Future purchaseOption(PurchaseOption option); +} diff --git a/lib/system/in_app_purchase/platform/in_app_purchase_html.dart b/lib/system/in_app_purchase/platform/in_app_purchase_html.dart new file mode 100644 index 00000000..a552b07b --- /dev/null +++ b/lib/system/in_app_purchase/platform/in_app_purchase_html.dart @@ -0,0 +1,6 @@ +// ignore: always_use_package_imports +import '../in_app_purchase.dart'; + +bool isPlatformSupported() => false; +LunaInAppPurchase getInAppPurchase() => + throw UnsupportedError('LunaInAppPurchase unsupported'); diff --git a/lib/system/in_app_purchase/platform/in_app_purchase_io.dart b/lib/system/in_app_purchase/platform/in_app_purchase_io.dart new file mode 100644 index 00000000..cac2413b --- /dev/null +++ b/lib/system/in_app_purchase/platform/in_app_purchase_io.dart @@ -0,0 +1,85 @@ +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:lunasea/core/state/state.dart'; +import 'package:lunasea/modules/settings/routes/donations/pages/thank_you.dart'; +import 'package:lunasea/system/in_app_purchase/purchase_option.dart'; +import 'package:lunasea/system/in_app_purchase/in_app_purchase.dart'; +import 'package:lunasea/system/logger.dart'; +import 'package:lunasea/system/platform.dart'; +import 'package:lunasea/vendor.dart'; +import 'package:lunasea/widgets/ui.dart'; + +bool isPlatformSupported() { + return LunaPlatform.isMobile; +} + +LunaInAppPurchase getInAppPurchase() { + if (LunaPlatform.isMobile) return _Mobile(); + throw UnsupportedError('LunaInAppPurchase unsupported'); +} + +class _Mobile implements LunaInAppPurchase { + static StreamSubscription>? _purchaseStream; + + static Future _purchaseCallback(List purchases) async { + for (final purchase in purchases) { + if (purchase.pendingCompletePurchase) { + await InAppPurchase.instance.completePurchase(purchase); + + switch (purchase.status) { + case PurchaseStatus.error: + showLunaErrorSnackBar( + title: 'lunasea.TransactionFailure'.tr(), + message: 'lunasea.PleaseTryAgain'.tr(), + ); + break; + case PurchaseStatus.purchased: + final context = LunaState.navigatorKey.currentContext!; + SettingsDonationsThankYouRouter().navigateTo(context); + break; + default: + break; + } + } + } + } + + InAppPurchase get _instance => InAppPurchase.instance; + + @override + Future initialize() async { + final available = await isAvailable(); + if (available) { + _purchaseStream = _instance.purchaseStream.listen(_purchaseCallback); + await getOptions(); + } else { + LunaLogger().debug('In-app purchasing disabled - unavailable'); + } + } + + @override + Future deinitialize() async { + await _purchaseStream?.cancel(); + } + + @override + Future isAvailable() async { + return _instance.isAvailable(); + } + + @override + Future> getOptions() async { + final keys = Set.from(PurchaseOption.keys); + final query = await _instance.queryProductDetails(keys); + + final donations = + query.productDetails.map((p) => p.toPurchaseOption()!).toList(); + donations.sort((a, b) => a.key.compareTo(b.key)); + return donations; + } + + @override + Future purchaseOption(PurchaseOption option) async { + final params = PurchaseParam(productDetails: option.product!); + await _instance.buyConsumable(purchaseParam: params, autoConsume: true); + } +} diff --git a/lib/system/in_app_purchase/platform/in_app_purchase_stub.dart b/lib/system/in_app_purchase/platform/in_app_purchase_stub.dart new file mode 100644 index 00000000..a552b07b --- /dev/null +++ b/lib/system/in_app_purchase/platform/in_app_purchase_stub.dart @@ -0,0 +1,6 @@ +// ignore: always_use_package_imports +import '../in_app_purchase.dart'; + +bool isPlatformSupported() => false; +LunaInAppPurchase getInAppPurchase() => + throw UnsupportedError('LunaInAppPurchase unsupported'); diff --git a/lib/system/in_app_purchase/purchase_option.dart b/lib/system/in_app_purchase/purchase_option.dart new file mode 100644 index 00000000..c3256423 --- /dev/null +++ b/lib/system/in_app_purchase/purchase_option.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:lunasea/system/logger.dart'; +import 'package:lunasea/vendor.dart'; + +const _DONATION_01_KEY = 'donation_01'; +const _DONATION_03_KEY = 'donation_03'; +const _DONATION_05_KEY = 'donation_05'; +const _DONATION_10_KEY = 'donation_10'; + +class PurchaseOption { + final String key; + final String title; + final IconData icon; + final ProductDetails? product; + + static List get keys { + return [ + _DONATION_01_KEY, + _DONATION_03_KEY, + _DONATION_05_KEY, + _DONATION_10_KEY, + ]; + } + + PurchaseOption._({ + required this.key, + required this.icon, + required this.title, + this.product, + }); + + factory PurchaseOption.donation01(ProductDetails? product) { + return PurchaseOption._( + key: _DONATION_01_KEY, + product: product, + icon: Icons.local_drink_rounded, + title: 'lunasea.BuyMeASoda'.tr(), + ); + } + + factory PurchaseOption.donation03(ProductDetails? product) { + return PurchaseOption._( + key: _DONATION_03_KEY, + product: product, + icon: Icons.local_cafe_rounded, + title: 'lunasea.BuyMeACoffee'.tr(), + ); + } + + factory PurchaseOption.donation05(ProductDetails? product) { + return PurchaseOption._( + key: _DONATION_05_KEY, + product: product, + icon: Icons.local_bar_rounded, + title: 'lunasea.BuyMeABeer'.tr(), + ); + } + + factory PurchaseOption.donation10(ProductDetails? product) { + return PurchaseOption._( + key: _DONATION_10_KEY, + product: product, + icon: Icons.fastfood_rounded, + title: 'lunasea.BuyMeABurger'.tr(), + ); + } +} + +extension ProductDetailsExtension on ProductDetails { + PurchaseOption? toPurchaseOption() { + switch (this.id) { + case _DONATION_01_KEY: + return PurchaseOption.donation01(this); + case _DONATION_03_KEY: + return PurchaseOption.donation03(this); + case _DONATION_05_KEY: + return PurchaseOption.donation05(this); + case _DONATION_10_KEY: + return PurchaseOption.donation10(this); + default: + LunaLogger().warning( + 'ProductDetailsExtension', + 'toDonation', + 'Unknown id: ${this.id}', + ); + return null; + } + } +} diff --git a/localization/lunasea/en.json b/localization/lunasea/en.json index 3b73f891..b0c677d3 100644 --- a/localization/lunasea/en.json +++ b/localization/lunasea/en.json @@ -66,6 +66,7 @@ "lunasea.Settings": "Settings", "lunasea.Stable": "Stable", "lunasea.StartingView": "Starting View", + "lunasea.TransactionFailure": "Transaction Failure", "lunasea.TryAgain": "Try Again", "lunasea.Tweaks": "Tweaks", "lunasea.Unknown": "Unknown",