diff --git a/lib/admin/add_package.dart b/lib/admin/add_package.dart index 29ee311..a3de6bd 100644 --- a/lib/admin/add_package.dart +++ b/lib/admin/add_package.dart @@ -11,6 +11,7 @@ import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; import '../utils/string_helper.dart'; import '../main.dart'; +import 'text_param.dart'; class AddPackage with Api { EditCardPackDto cardPackDto; @@ -96,26 +97,26 @@ class AddPackage with Api { ], ), ), - _TextParam( + TextParam( 'Title', cardPackDto.title, (v) { cardPackDto = cardPackDto.copyWith.title(v); }, ), - _TextParam( + TextParam( 'Subtitle', cardPackDto.subtitle, (v) { cardPackDto = cardPackDto.copyWith.subtitle(v); }, ), - _TextParam( + TextParam( 'Description', cardPackDto.description, (v) => cardPackDto = cardPackDto.copyWith.description(v), ), - _TextParam( + TextParam( 'Size', cardPackDto.size?.toString(), (v) { @@ -124,28 +125,28 @@ class AddPackage with Api { ); }, ), - _TextParam( + TextParam( 'Google play id', cardPackDto.googlePlayId, (v) { cardPackDto = cardPackDto.copyWith.googlePlayId(v); }, ), - _TextParam( + TextParam( 'Rustore id', cardPackDto.rustoreId, (v) { cardPackDto = cardPackDto.copyWith.rustoreId(v); }, ), - _TextParam( + TextParam( 'Appstore id', cardPackDto.appStoreId, (v) { cardPackDto = cardPackDto.copyWith.appStoreId(v); }, ), - _TextParam( + TextParam( 'Price', cardPackDto.price, (v) { @@ -254,7 +255,7 @@ class AddPackage with Api { ), ); }), - _TextParam( + TextParam( 'Preview cards', cardPackDto.previewCards?.toString(), (v) { @@ -265,7 +266,7 @@ class AddPackage with Api { cardPackDto = cardPackDto.copyWith.previewCards(cards); }, ), - _TextParam( + TextParam( 'Order (lower is higher)', cardPackDto.order?.toString(), (v) { @@ -274,7 +275,7 @@ class AddPackage with Api { ); }, ), - _TextParam( + TextParam( 'Cards order (ids)', cardPackDto.cardsOrder?.join(','), (v) { @@ -295,28 +296,3 @@ class AddPackage with Api { } } -class _TextParam extends StatelessWidget { - final String title; - final String? initial; - final Function(String v) onChanged; - - _TextParam(this.title, this.initial, this.onChanged); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Text(title), - Expanded( - child: TextField( - controller: TextEditingController(text: initial), - onChanged: onChanged, - ), - ), - ], - ), - ); - } -} diff --git a/lib/admin/add_plan.dart b/lib/admin/add_plan.dart new file mode 100644 index 0000000..03c3014 --- /dev/null +++ b/lib/admin/add_plan.dart @@ -0,0 +1,335 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:file_picker/file_picker.dart'; +import 'package:mnemo_cards/admin/admin_api.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; + +import '../di/injector.dart'; +import '../main.dart'; +import 'text_param.dart'; + +class AddPlan { + List allPlans = []; + String? planId = null; + List selectedFeatures = []; + late SubscriptionPlanAdminDto planDto; + late AdminApi adminApi; + + final availableFeatures = [ + SubscriptionFeatureEnum.ads, + SubscriptionFeatureEnum.packs, + ]; + + AddPlan() { + this.adminApi = getIt.get(); + _init(null); + } + + void _init(SubscriptionPlanAdminDto? dto) { + planId = dto?.id; + planDto = dto ?? + const SubscriptionPlanAdminDto( + ui: SubscriptionPlanUIDto( + currency: 'руб', + ), + price: '123', + currency: 'руб', + durationDays: 0, + features: [ + SubscriptionFeatureEnum.ads, + SubscriptionFeatureEnum.packs, + ], + ); + selectedFeatures = planDto.features.toList(); + } + + Future _loadPlans() async { + final result = await adminApi.getSubscriptionPlans(); + allPlans = result; + return true; + } + + void _saveSubscriptionPlan() async { + final result = await adminApi.addSubscriptionPlan( + planDto.copyWith( + features: selectedFeatures, + ), + ); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void _deletePLan() async { + if (planDto.id == null) return; + final result = await adminApi.deleteSubscriptionPlan(planDto.id.toString()); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void addPlan(BuildContext context) { + showDialog( + context: context, + builder: (c) { + return FutureBuilder( + future: _loadPlans(), + builder: (context, snapshot) { + if (snapshot.data != true) { + return Center( + child: Material( + child: Center(child: CircularProgressIndicator()), + ), + ); + } + return Center( + child: Scaffold( + body: Material( + child: StatefulBuilder(builder: (context, setPlan) { + return ListView( + children: [ + Header( + '${planDto.id}', + hasPopButton: true, + trail: Row( + children: [ + IconButton( + onPressed: () async { + await _loadPlans(); + setPlan(() {}); + }, + icon: Icon(Icons.sync), + ), + IconButton( + onPressed: _saveSubscriptionPlan, + icon: Icon(Icons.save), + ), + if (planDto.id != null) + GestureDetector( + onLongPress: _deletePLan, + child: Icon(Icons.delete), + ) + ], + ), + ), + DropdownMenu( + initialSelection: null, + onSelected: (String? value) { + setPlan(() { + _init( + allPlans + .firstWhereOrNull((v) => v.id == value), + ); + }); + }, + dropdownMenuEntries: [ + ...allPlans, + null, + ].map((plan) { + return DropdownMenuEntry( + value: plan?.id, + label: plan == null + ? 'New' + : '${plan.ui.titleLead ?? ''} ${plan.ui.title ?? ''}', + ); + }).toList(), + ), + StatefulBuilder(builder: (context, setState) { + return Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + TextParam( + 'Currency', + planDto.currency, + (v) { + planDto = planDto.copyWith( + currency: v); + }, + ), + TextParam( + 'Price', + planDto.price, + (v) { + planDto = + planDto.copyWith(price: v); + }, + ), + TextParam( + 'Days', + planDto.durationDays.toString(), + (v) { + final days = int.parse(v); + planDto = planDto.copyWith( + durationDays: days, + ); + }, + ), + Text('Features:'), + ListView.builder( + shrinkWrap: true, + itemCount: + availableFeatures.length, + itemBuilder: (context, index) { + final item = + availableFeatures[index]; + return ListTile( + title: Text(item.name), + onTap: () { + setState(() { + if (selectedFeatures + .contains(item)) { + selectedFeatures + .remove(item); + } else { + selectedFeatures + .add(item); + } + }); + }, + selected: selectedFeatures + .contains(item), + selectedTileColor: Colors.blue + .withOpacity(0.5), + ); + }, + ), + Text('UI:'), + TextParam( + 'UI Currency', + planDto.ui.currency, + (v) { + final ui = planDto.ui.copyWith( + currency: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + TextParam( + 'UI Title lead', + planDto.ui.titleLead, + (v) { + final ui = planDto.ui.copyWith( + titleLead: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + TextParam( + 'UI Title', + planDto.ui.title, + (v) { + final ui = planDto.ui.copyWith( + title: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + TextParam( + 'UI Price per month', + planDto.ui.pricePerMonth, + (v) { + final ui = planDto.ui.copyWith( + pricePerMonth: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + TextParam( + 'UI Subtitle crossed', + planDto.ui.subtitleCrossed, + (v) { + final ui = planDto.ui.copyWith( + subtitleCrossed: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + TextParam( + 'UI Subtitle', + planDto.ui.subtitle, + (v) { + final ui = planDto.ui.copyWith( + subtitle: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + TextParam( + 'UI Tip', + planDto.ui.tip, + (v) { + final ui = planDto.ui.copyWith( + tip: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + TextParam( + 'UI Tip color', + planDto.ui.tipColor, + (v) { + final ui = planDto.ui.copyWith( + tipColor: v, + ); + planDto = + planDto.copyWith.ui(ui); + }, + ), + ], + ), + ], + ), + ), + ], + ); + }), + ], + ); + }), + ), + ), + ); + }); + }); + } + + Widget uiEditor() { + return Column( + children: [], + ); + } +} diff --git a/lib/admin/admin_api.dart b/lib/admin/admin_api.dart index 6ed35e3..93df968 100644 --- a/lib/admin/admin_api.dart +++ b/lib/admin/admin_api.dart @@ -111,4 +111,29 @@ class AdminApi with Api { Future> packs() => getIt.get().getPacksPreviews(null); + + Future> getSubscriptionPlans() async { + final r = await _dio.get( + '$path/subscriptions', + ); + return (jsonDecode(r.data!)['plans'] as List) + .cast>() + .map(SubscriptionPlanAdminDto.fromJson) + .toList(); + } + + Future addSubscriptionPlan(SubscriptionPlanAdminDto dto) async { + final r = await _dio.post( + '$path/subscription/add', + data: jsonEncode(dto.toJson()), + ); + return r.statusCode == 200; + } + + Future deleteSubscriptionPlan(String id) async { + final r = await _dio.post( + '$path/subscription/delete/$id', + ); + return r.statusCode == 200; + } } diff --git a/lib/admin/text_param.dart b/lib/admin/text_param.dart new file mode 100644 index 0000000..1ca9f12 --- /dev/null +++ b/lib/admin/text_param.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class TextParam extends StatelessWidget { + final String title; + final String? initial; + final Function(String v) onChanged; + + TextParam(this.title, this.initial, this.onChanged); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Text(title), + Expanded( + child: TextField( + controller: TextEditingController(text: initial), + onChanged: onChanged, + ), + ), + ], + ), + ); + } +} diff --git a/lib/di/injector.dart b/lib/di/injector.dart index 21331c7..4c13593 100644 --- a/lib/di/injector.dart +++ b/lib/di/injector.dart @@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart'; import 'package:mnemo_cards/admin/admin_api.dart'; import 'package:mnemo_cards/features/packs/pack_cache_manager.dart'; import 'package:mnemo_cards/features/packs/packs_api.dart'; +import 'package:mnemo_cards/features/subscription/subscription_api.dart'; import 'package:mnemo_cards/flags.dart'; import 'package:mnemo_cards/managers/favorite_cards.dart'; import 'package:mnemo_cards/managers/repository/dio_provider.dart'; @@ -35,6 +36,11 @@ Future setInjections() async { ); print('set pack'); + getIt.registerLazySingleton( + () => SubscriptionApi(getIt.get().dio), + ); + print('set subscription'); + if (ADMIN_BUILD) { getIt.registerLazySingleton( () => AdminApi(getIt.get().dio)); diff --git a/lib/di/locator.dart b/lib/di/locator.dart index e5bfe9b..57081dc 100644 --- a/lib/di/locator.dart +++ b/lib/di/locator.dart @@ -9,6 +9,7 @@ import '../features/packs/pack_cache_manager.dart'; import '../features/packs/pack_updater.dart'; import '../features/purchase/in_app_purchase.dart'; import '../features/packs/pack_manager.dart'; +import '../features/subscription/subscription_api.dart'; import '../features/tests/test_manager.dart'; import '../managers/user_manager.dart'; import 'injector.dart'; @@ -26,6 +27,8 @@ class Locator { UserManager get userManager => _getIt.get(); + SubscriptionApi get subscriptionApi => _getIt.get(); + FavoriteCardsController get favoriteCardsController => _getIt.get(); diff --git a/lib/domain/router/dialogs.dart b/lib/domain/router/dialogs.dart index fd00dfd..57b87cf 100644 --- a/lib/domain/router/dialogs.dart +++ b/lib/domain/router/dialogs.dart @@ -1,60 +1,73 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:mnemo_cards/theme/themes.dart'; import '../../main.dart'; -showErrorDialog(String message) { - return showDialog( - context: scaffoldKey.currentContext!, - builder: (context) { - return Center( - child: Material( - color: Colors.transparent, - child: Container( - margin: EdgeInsets.all(4.0.h), - padding: EdgeInsets.all(4.0.h), - height: 200.h, - width: MediaQuery.of(context).size.width * 0.8, - decoration: BoxDecoration( - color: white, - border: Border.all( - color: Colors.grey.withOpacity(0.5), - width: 1, - ), - borderRadius: BorderRadius.circular(12.0), +Future showErrorDialog(String message, {Completer? completer}) { + final context = scaffoldKey.currentContext!; + completer ??= Completer(); + final result = showDialog( + context: context, + useRootNavigator: false, + builder: (context) { + return Center( + child: Material( + color: Colors.transparent, + child: Container( + margin: EdgeInsets.all(4.0.h), + padding: EdgeInsets.all(4.0.h), + height: 200.h, + width: MediaQuery.of(context).size.width * 0.8, + decoration: BoxDecoration( + color: white, + border: Border.all( + color: Colors.grey.withOpacity(0.5), + width: 1, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Center( - child: Text( - message, - textAlign: TextAlign.center, - ), + borderRadius: BorderRadius.circular(12.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Center( + child: Text( + message, + textAlign: TextAlign.center, ), ), - GestureDetector( - onTap: Navigator.of(context).pop, - child: Container( - height: 40.h, - decoration: BoxDecoration( - color: testBlue, - border: Border.all( - color: Colors.grey.withOpacity(0.5), - width: 1, - ), - borderRadius: BorderRadius.circular(8.0), + ), + GestureDetector( + onTap: Navigator.of(context).pop, + child: Container( + height: 40.h, + decoration: BoxDecoration( + color: testBlue, + border: Border.all( + color: Colors.grey.withOpacity(0.5), + width: 1, ), - alignment: Alignment.center, - child: Text('Оки'), + borderRadius: BorderRadius.circular(8.0), ), - ) - ], - ), + alignment: Alignment.center, + child: Text('Оки'), + ), + ) + ], ), ), - ); - }); + ), + ); + }, + ); + bool dialogClosed = false; + completer.future.then((_) { + if (!dialogClosed) { + return Navigator.of(context).pop(); + } + }); + return result.then((_) => dialogClosed = true); } diff --git a/lib/features/packs/pack_cache_manager.dart b/lib/features/packs/pack_cache_manager.dart index 3dafb56..54add5e 100644 --- a/lib/features/packs/pack_cache_manager.dart +++ b/lib/features/packs/pack_cache_manager.dart @@ -33,7 +33,7 @@ class PackCacheManager { await file.writeAsString(jsonEncode(json)); } } catch (e, s) { - log('Cant marck pack for update', error: e, stackTrace: s); + log('Cant mark pack for update', error: e, stackTrace: s); Analytics.crashlyticsError( e: e, s: s, diff --git a/lib/features/packs/pack_updater.dart b/lib/features/packs/pack_updater.dart index c35a64d..4937033 100644 --- a/lib/features/packs/pack_updater.dart +++ b/lib/features/packs/pack_updater.dart @@ -2,14 +2,14 @@ import 'dart:async'; import 'dart:developer'; import 'dart:typed_data'; -import 'package:archive/archive_io.dart' as a; -import 'package:flutter/cupertino.dart'; -import 'package:mnemo_cards/features/packs/packs_api.dart'; +import 'package:archive/archive_io.dart'; +import 'package:flutter/widgets.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import '../../di/locator.dart'; -import '../../features/packs/pack_cache_manager.dart'; -import '../../features/packs/pack_holder.dart'; +import '../analytics/analytics.dart'; +import 'pack_cache_manager.dart'; +import 'pack_holder.dart'; +import 'packs_api.dart'; import 'preview_pack_holder.dart'; /// Updates cache and state holders for available packs @@ -33,15 +33,19 @@ class PackUpdater { final pollStream = Stream.periodic(const Duration(seconds: 600)); pollStream.listen((event) async { await updateAvailablePacks(); - try { - final r = await _packsApi.getPacksPreviews(null); - _previewPackHolder.setPreviewPacks(r, save: true); - } catch (e) { - log(e.toString()); - } + await _updatePreviewPacks(); }); } + Future _updatePreviewPacks() async { + try { + final r = await _packsApi.getPacksPreviews(null); + _previewPackHolder.setPreviewPacks(r, save: true); + } catch (e) { + log(e.toString()); + } + } + Future updateAvailablePacks() async { if (_updateCompleter?.isCompleted ?? true) { _updateCompleter = Completer(); @@ -106,8 +110,10 @@ class PackUpdater { } } - Future updatePackAndSaveImages(String packId, - {bool shouldUpdateHolder = false}) async { + Future updatePackAndSaveImages( + String packId, { + bool shouldUpdateHolder = false, + }) async { try { log('Updating with cards ${packId}', name: 'updatePackWithCards'); final s = Stopwatch()..start(); @@ -118,14 +124,13 @@ class PackUpdater { } final imagesData = await _packsApi.packImagesArchive(packId); print('get cards from api ${s.elapsedMilliseconds} mills'); - final userId = locator.userManager.userId!; final appVersion = (await PackageInfo.fromPlatform()).version; final password = TokenGenerator.generateArchivePassword( appVersion: appVersion, packId: packId, ); print('generate token ${s.elapsedMilliseconds} mills'); - final imagesArchive = a.ZipDecoder().decodeBytes( + final imagesArchive = ZipDecoder().decodeBytes( imagesData, password: password, ); @@ -133,7 +138,7 @@ class PackUpdater { Map images = {}; // todo optimize, make really async - Future setImage(a.ArchiveFile file) async { + Future setImage(ArchiveFile file) async { final fileData = file.content as List; final data = Uint8List.fromList(fileData); images[file.name] = MemoryImage(data); @@ -143,7 +148,6 @@ class PackUpdater { for (final file in imagesArchive) setImage(file), ]); - print('loaded images ${s.elapsedMilliseconds} mills'); _cacheManager.savePack(packDto, images: images); print('saved pack ${s.elapsedMilliseconds} mills'); @@ -156,8 +160,12 @@ class PackUpdater { } return packDto; } catch (e, s) { - log('Cant update pack', name: 'updatePackWithCards'); - log(e.toString(), stackTrace: s); + log('Cant update pack', error: e, stackTrace: s); + Analytics.crashlyticsError( + message: 'Cant update pack', + e: e, + s: s, + ); rethrow; } } diff --git a/lib/features/purchase/in_app_purchase.dart b/lib/features/purchase/in_app_purchase.dart index 3e98c4b..2f7d87b 100644 --- a/lib/features/purchase/in_app_purchase.dart +++ b/lib/features/purchase/in_app_purchase.dart @@ -185,9 +185,9 @@ class PurchaseDetailsStreamSubscription { }, ); final result = await locator.repository.checkPayment( - purchaseDetails.productID, - purchaseDetails.verificationData.serverVerificationData, - androidPaymentSystem, + itemId: purchaseDetails.productID, + key: purchaseDetails.verificationData.serverVerificationData, + paymentSystem: androidPaymentSystem, ); if (result) { Analytics.buyComplete( diff --git a/lib/features/purchase/rustore/mnemo_rustore.dart b/lib/features/purchase/rustore/mnemo_rustore.dart index 3e391c4..cba00d2 100644 --- a/lib/features/purchase/rustore/mnemo_rustore.dart +++ b/lib/features/purchase/rustore/mnemo_rustore.dart @@ -54,9 +54,9 @@ class RustorePurchaseService { // creating payment to check it later // in case there will be an error after confirmation await locator.repository.checkPayment( - result.successPurchase!.productId, - result.successPurchase!.subscriptionToken!, - androidPaymentSystem, + itemId: result.successPurchase!.productId, + key: result.successPurchase!.subscriptionToken!, + paymentSystem: androidPaymentSystem, ); } catch (e, s) { Analytics.buyError( @@ -70,13 +70,13 @@ class RustorePurchaseService { return false; } final p = await RustoreBillingClient.confirm( - result.successPurchase!.purchaseId, + result.successPurchase!.subscriptionToken!, ); if (p.success) { final r = await locator.repository.checkPayment( - result.successPurchase!.productId, - result.successPurchase!.subscriptionToken!, - androidPaymentSystem, + itemId: result.successPurchase!.productId, + key: result.successPurchase!.subscriptionToken!, + paymentSystem: androidPaymentSystem, ); if (r) { return r; diff --git a/lib/features/subscription/subscription_api.dart b/lib/features/subscription/subscription_api.dart index dcbc516..85ba167 100644 --- a/lib/features/subscription/subscription_api.dart +++ b/lib/features/subscription/subscription_api.dart @@ -10,16 +10,15 @@ class SubscriptionApi with Api { SubscriptionApi(this._dio); - // buy page dto if not available - Future getPackBuy(String id) async { + Future getBuySubscription() async { final r = await _dio.get( - '$path/pack/buy/$id', + '$path/subscription/page', options: Options( sendTimeout: Duration(seconds: 5), receiveTimeout: Duration(seconds: 5), ), ); - return (r.data as String).decode(CardPackBuyDto.fromJson); + return (r.data as String).decode(SubscriptionDto.fromJson); } /// cards zip archive for available pack diff --git a/lib/features/yandex_ads/yandex_ads.dart b/lib/features/yandex_ads/yandex_ads.dart index 9820f2f..1a0981f 100644 --- a/lib/features/yandex_ads/yandex_ads.dart +++ b/lib/features/yandex_ads/yandex_ads.dart @@ -1,9 +1,13 @@ import 'dart:async'; import 'dart:developer'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; +import 'package:mnemo_cards/domain/router/dialogs.dart'; import 'package:yandex_mobileads/mobile_ads.dart'; +import '../../main.dart'; + class YandexAds { static BannerAd createBanner( BoxConstraints constraints, { @@ -42,6 +46,7 @@ class YandexAds { } static RewardedAd? _preloadedAd; + static Completer? _showingAdCompleter; static Future loadRewardedAd() async { final completer = Completer(); @@ -66,30 +71,58 @@ class YandexAds { Function(Reward reward) onRewarded, { maxAttempts = 2, }) async { - if (_preloadedAd == null) { - final _loadedAd = await loadRewardedAd(); - if (_loadedAd == null) { - return false; - } + if (_showingAdCompleter?.isCompleted == false) { + return false; } - final _ad = _preloadedAd; - _preloadedAd = null; - _ad?.setAdEventListener( - eventListener: RewardedAdEventListener(onAdDismissed: () { - _ad.destroy(); - }, onAdFailedToShow: (e) { - _ad.destroy(); - if (maxAttempts-- > 0) { - showRewardedAd(onRewarded, maxAttempts: maxAttempts); + try { + _showingAdCompleter = Completer(); + if (_preloadedAd == null) { + // final dialogCompleter = Completer(); + // final dialogFuture = + // showErrorDialog('Загрузка...', completer: dialogCompleter); + try { + final _loadedAd = await loadRewardedAd().timeout( + Duration(seconds: 5), + ); + if (_loadedAd == null) { + return false; + } + } finally {} + // dialogCompleter.complete(); + // await dialogFuture; + // final frameCompleter = Completer(); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // frameCompleter.complete(); + // }); + // await frameCompleter; } - }, onAdShown: () { - // _ad.destroy(); - }, onRewarded: (r) { - onRewarded(r); - _ad.destroy(); - })); - await _ad?.show(); - final reward = await _ad?.waitForDismiss(); - return reward != null; + final _ad = _preloadedAd; + _preloadedAd = null; + await _ad?.setAdEventListener( + eventListener: RewardedAdEventListener( + onAdDismissed: () { + _ad.destroy(); + }, + onAdFailedToShow: (e) { + _ad.destroy(); + if (maxAttempts-- > 0) { + showRewardedAd(onRewarded, maxAttempts: maxAttempts); + } + }, + onAdShown: () { + // _ad.destroy(); + }, + onRewarded: (r) { + onRewarded(r); + _ad.destroy(); + }, + ), + ); + await _ad?.show(); + final reward = await _ad?.waitForDismiss(); + return reward != null; + } finally { + _showingAdCompleter?.complete(); + } } } diff --git a/lib/managers/repository/api.dart b/lib/managers/repository/api.dart index 54fcd53..c209382 100644 --- a/lib/managers/repository/api.dart +++ b/lib/managers/repository/api.dart @@ -9,7 +9,7 @@ mixin Api { ? 'https://89.19.210.178:8080' //FirebaseRepository.appConfig.path : androidDeviceInfo!.isPhysicalDevice //device to localhost - ? 'https://192.168.31.158:8080' + ? 'https://192.168.31.159:8080' : //emulator to localhost 'https://10.0.2.2:8080'; } diff --git a/lib/managers/repository/http_repository.dart b/lib/managers/repository/http_repository.dart index d87cf43..b106980 100644 --- a/lib/managers/repository/http_repository.dart +++ b/lib/managers/repository/http_repository.dart @@ -119,11 +119,11 @@ class HttpRepository extends Repository with Api { } @override - Future checkPayment( - String itemId, - String key, - PaymentSystem paymentSystem, - ) async { + Future checkPayment({ + required String itemId, + required String key, + required PaymentSystem paymentSystem, + }) async { final r = await _dio.get( '$path/check_payment/', queryParameters: { diff --git a/lib/managers/repository/repository.dart b/lib/managers/repository/repository.dart index 4b82a0b..ed782df 100644 --- a/lib/managers/repository/repository.dart +++ b/lib/managers/repository/repository.dart @@ -31,11 +31,11 @@ abstract class Repository { Future getTest(String id); - Future checkPayment( - String itemId, - String key, - PaymentSystem PaymentSystem, - ); + Future checkPayment({ + required String itemId, + required String key, + required PaymentSystem paymentSystem, + }); Future checkUserPayment(); diff --git a/lib/managers/user_manager.dart b/lib/managers/user_manager.dart index fac67dd..b2072cf 100644 --- a/lib/managers/user_manager.dart +++ b/lib/managers/user_manager.dart @@ -34,7 +34,9 @@ class UserManager { UserManager(this._repository) : userStateHolder = UserStateHolder(); - bool get hasUser => userStateHolder.user != null; + bool get hasUser => user != null; + + UserDto? get user => userStateHolder.user; int? get userId => userStateHolder.user?.id; diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index 8e7c207..34d4797 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/admin/add_plan.dart'; import 'package:mnemo_cards/domain/router/app_router.dart'; import 'package:mnemo_cards/domain/router/app_router.gr.dart'; import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart'; @@ -18,6 +19,7 @@ import '../di/locator.dart'; import '../domain/router/dialogs.dart'; import '../features/analytics/analytics.dart'; import '../theme/themes.dart'; +import '../widgets/buy_subscription_widget.dart'; import '../widgets/enter_promo_code_widget.dart'; import '../widgets/shared_pref_button.dart'; @@ -75,7 +77,7 @@ class ProfilePage extends StatelessWidget { context: context, builder: (c) => const AllCards()); }, - child: Text('ALL CARDS'), + child: Text('Cards'), ), ), Expanded( @@ -85,7 +87,15 @@ class ProfilePage extends StatelessWidget { context: context, builder: (c) => const AllUsers()); }, - child: Text('ALL USERS'), + child: Text('Users'), + ), + ), + Expanded( + child: MaterialButton( + onPressed: () { + AddPlan().addPlan(context); + }, + child: Text('Plans'), ), ), ], @@ -146,6 +156,11 @@ class ProfilePage extends StatelessWidget { height: 5, color: borderGray.withOpacity(0.2), ), + const MobileBuySubscriptionWidget(), + Divider( + height: 5, + color: borderGray.withOpacity(0.2), + ), if (SHOW_ADMIN) SimpleTile(title: Text('Посмотреть рекламу'), onTap: () { YandexAds.showRewardedAd((r) { diff --git a/lib/widgets/buy_subscription_widget.dart b/lib/widgets/buy_subscription_widget.dart index 7cae818..070b32a 100644 --- a/lib/widgets/buy_subscription_widget.dart +++ b/lib/widgets/buy_subscription_widget.dart @@ -1,14 +1,20 @@ +import 'dart:math'; + import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:mnemo_cards/main.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; import '../di/injector.dart'; import '../di/locator.dart'; import '../domain/router/app_router.dart'; import '../features/packs/packs_api.dart'; +import '../theme/themes.dart'; class MobileBuySubscriptionWidget extends StatelessWidget { - @override Widget build(BuildContext context) { return BuySubscriptionWidget(onBuySubscription: onBuySubscription); @@ -20,12 +26,188 @@ class MobileBuySubscriptionWidget extends StatelessWidget { AppRouter.openAuthOrProfile(); return; } - if (user.subscription) { - return; + final api = locator.subscriptionApi; + final buyDto = await api.getBuySubscription().catchError((_) => null); + if (buyDto != null) { + showDialog( + context: scaffoldKey.currentContext!, + builder: (_) => SubscriptionPage(buyDto.page!), + ); } - final packsApi = getIt.get(); - // final buyDto = await packsApi.getPackBuy(id).catchError((_) => null); } const MobileBuySubscriptionWidget(); -} \ No newline at end of file +} + +class SubscriptionPage extends StatelessWidget { + final SubscriptionPageDto dto; + + SubscriptionPage(this.dto); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Header(dto.title, hasPopButton: true, center: true), + ...dto.items.build(), + ...dto.plans.map(_SubscriptionPlanWidget.new), + BigBackButton(), + ], + ), + ); + } +} + +class _SubscriptionPlanWidget extends StatelessWidget { + final SubscriptionPlanDto plan; + + _SubscriptionPlanWidget(this.plan); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: 10.0, right: 10.0), + height: (90 + (plan.tip == null ? 0 : 13)).h, + child: GestureDetector( + onTap: () { + + }, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Padding( + padding: EdgeInsets.only(top: plan.tip == null ? 0 : 13.h), + child: Container( + height: 80.h, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + border: Border.all(color: borderGray), + borderRadius: BorderRadius.circular(12.0), + ), + padding: EdgeInsets.only( + left: 10.0.w, + right: 20.w, + top: 10.h, + bottom: 10.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: _lead(plan)), + SizedBox( + width: 85.w, + child: _trail(plan), + ), + ], + ), + ), + ), + if (plan.tip != null) + Container( + height: 26.h, + width: 110.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: plan.tipColor?.asColor ?? progressBlue, + border: Border.all(color: borderGray), + borderRadius: BorderRadius.circular(16.0), + ), + padding: EdgeInsets.all(2.0.w), + child: Text( + plan.tip!, + style: TextStyle(color: white), + ), + ), + ], + ), + ), + ); + } + + Widget _lead(SubscriptionPlanDto plan) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text.rich( + TextSpan( + children: [ + if (plan.titleLead != null) + TextSpan( + text: plan.titleLead, + style: TextStyle(fontSize: 32), + ), + if (plan.title != null) + TextSpan( + text: ' ${plan.title}', + style: TextStyle( + fontSize: 16, + ), + ), + ], + style: TextStyle( + height: 0.85, + )), + ), + Text.rich( + TextSpan( + children: [ + if (plan.subtitleCrossed != null) + TextSpan( + text: plan.subtitleCrossed, + style: TextStyle( + fontSize: 20, + color: borderGray, + decoration: TextDecoration.lineThrough, + height: 0.85, + ), + ), + if (plan.subtitleCrossed != null && plan.subtitle != null) + TextSpan(text: ' '), + if (plan.subtitle != null) + TextSpan( + text: plan.subtitle, + style: TextStyle( + fontSize: 20.h, + height: 0.85, + ), + ), + ], + ), + ) + ], + ); + } + + Widget _trail(SubscriptionPlanDto plan) { + if (plan.pricePerMonth != null) + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text.rich( + TextSpan(children: [ + TextSpan( + text: plan.pricePerMonth!, + style: TextStyle(fontSize: 24), + ), + TextSpan( + text: ' ${plan.currency}', + style: TextStyle( + fontSize: 14, + ), + ), + ], style: TextStyle(height: 0.85)), + ), + Text( + 'в месяц', + style: TextStyle( + fontSize: 14, + ), + ), + ], + ); + return SizedBox.shrink(); + } +} diff --git a/lib/widgets/card_pack/available_pack.dart b/lib/widgets/card_pack/available_pack.dart index 0f6131e..7523b23 100644 --- a/lib/widgets/card_pack/available_pack.dart +++ b/lib/widgets/card_pack/available_pack.dart @@ -127,7 +127,7 @@ class AvailablePack extends StatelessWidget { ], ); }), - _AdTile(), + if (locator.userManager.user?.subscription != true) _AdTile(), _TestsSection(cardPack.id.toString()), ], ), @@ -257,9 +257,8 @@ class _AdTile extends StatelessWidget { if (kIsWeb) return SizedBox.shrink(); return LayoutBuilder(builder: (context, constraints) { final banner = YandexAds.createBanner( - BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)), - id: 'R-M-3761037-2' - ); + BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)), + id: 'R-M-3761037-2'); return FadeInWidget( future: banner.loadAd(), animationDuration: Duration(milliseconds: 1000), diff --git a/lib/widgets/card_pack/buy_pack_page.dart b/lib/widgets/card_pack/buy_pack_page.dart index ce98a1b..8b7be8c 100644 --- a/lib/widgets/card_pack/buy_pack_page.dart +++ b/lib/widgets/card_pack/buy_pack_page.dart @@ -5,9 +5,9 @@ import 'package:mnemo_cards/utils/string_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; import '../../features/analytics/analytics.dart'; import '../../main.dart'; -import '../items/builder.dart'; import '../../di/locator.dart'; import '../../domain/router/app_router.dart'; diff --git a/lib/widgets/game_card_widget.dart b/lib/widgets/game_card_widget.dart index c725568..024826c 100644 --- a/lib/widgets/game_card_widget.dart +++ b/lib/widgets/game_card_widget.dart @@ -61,10 +61,8 @@ class GameCardDecoration extends StatelessWidget { ), decoration: BoxDecoration( // color: Theme.of(context).scaffoldBackgroundColor, - color: Theme.of(context) - .scaffoldBackgroundColor - .lighten(0.05), - // .withOpacity(alpha), + color: Theme.of(context).scaffoldBackgroundColor.lighten(0.05), + // .withOpacity(alpha), boxShadow: [ BoxShadow( color: Theme.of(context) @@ -390,45 +388,3 @@ class FrontCard extends StatelessWidget { }); } } - -class _BannerWidget extends StatelessWidget { - final BoxConstraints constraints; - final String adId; - final String adKey; - late final BannerAd bannerAd; - - bool bannerLoaded = false; - - _BannerWidget(this.constraints, this.adId, this.adKey) - : super(key: ValueKey(adKey)); - - @override - Widget build(BuildContext context) { - BannerAd? bannerAd; - bool bannerLoaded = false; - return StatefulBuilder(builder: (context, setState) { - bannerAd ??= YandexAds.createBanner( - BoxConstraints( - maxHeight: constraints.maxHeight, - maxWidth: constraints.maxWidth, - ), - id: adId, - key: adKey, - onLoaded: () { - bannerLoaded = true; - setState(() {}); - }, - ); - return (bannerLoaded || true) - ? Container( - alignment: Alignment.center, - child: AdWidget( - bannerAd: bannerAd!, - ), - ) - : Center( - child: CircularProgressIndicator(), - ); - }); - } -} diff --git a/lib/widgets/items/builder.dart b/lib/widgets/items/builder.dart deleted file mode 100644 index 8238364..0000000 --- a/lib/widgets/items/builder.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:mnemo_cards/theme/themes.dart'; -import 'package:mnemo_cards/utils/string_helper.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; - -class ItemBuilder { - static Widget build(Item item) { - switch (item.type) { - case ItemType.text: - return TextItemBuilder(item as TextItem); - default: - log('Undefined item type ${item.type} ${item.id}'); - return SizedBox.shrink(); - } - } -} - -extension ItemExt on Item { - Widget build() => ItemBuilder.build(this); -} - -extension ItemIterableExt on Iterable { - Iterable build() => map((e) => e.build()); -} - -class TextItemBuilder extends StatelessWidget { - final TextItem item; - - TextItemBuilder(this.item); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(8.0), - decoration: BoxDecoration( - border: item.hasBorder - ? Border.all( - color: item.color?.asColor ?? borderGray, - ) - : null, - ), - child: Column( - children: [ - if (item.title != null) - Text( - item.title!, - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 24, - color: Colors.black, - ), - textAlign: TextAlign.center, - ), - if (item.subtitle != null) - Text( - item.subtitle!, - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 16, - color: Colors.black, - ), - ) - ], - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 00a1925..2e5c735 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -576,6 +576,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_widget_from_html_core: + dependency: "direct main" + description: + name: flutter_widget_from_html_core + sha256: df7c7c9e5ea144f7ab0adfbad733b4d4f7d408ab733c94e6e9fdcb327af92aa1 + url: "https://pub.dev" + source: hosted + version: "0.15.1" freezed: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index dc3bc90..956e1f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: flutter_colorpicker: ^1.1.0 jailbreak_root_detection: ^1.1.1 no_screenshot: ^0.0.1+6 + flutter_widget_from_html_core: ^0.15.1 file_picker: url_launcher: ^6.2.6