This commit is contained in:
Dmitry 2024-08-04 01:37:10 +03:00
parent 5a37539554
commit 35725aeaa1
25 changed files with 800 additions and 280 deletions

View file

@ -11,6 +11,7 @@ import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
import '../utils/string_helper.dart'; import '../utils/string_helper.dart';
import '../main.dart'; import '../main.dart';
import 'text_param.dart';
class AddPackage with Api { class AddPackage with Api {
EditCardPackDto cardPackDto; EditCardPackDto cardPackDto;
@ -96,26 +97,26 @@ class AddPackage with Api {
], ],
), ),
), ),
_TextParam( TextParam(
'Title', 'Title',
cardPackDto.title, cardPackDto.title,
(v) { (v) {
cardPackDto = cardPackDto.copyWith.title(v); cardPackDto = cardPackDto.copyWith.title(v);
}, },
), ),
_TextParam( TextParam(
'Subtitle', 'Subtitle',
cardPackDto.subtitle, cardPackDto.subtitle,
(v) { (v) {
cardPackDto = cardPackDto.copyWith.subtitle(v); cardPackDto = cardPackDto.copyWith.subtitle(v);
}, },
), ),
_TextParam( TextParam(
'Description', 'Description',
cardPackDto.description, cardPackDto.description,
(v) => cardPackDto = cardPackDto.copyWith.description(v), (v) => cardPackDto = cardPackDto.copyWith.description(v),
), ),
_TextParam( TextParam(
'Size', 'Size',
cardPackDto.size?.toString(), cardPackDto.size?.toString(),
(v) { (v) {
@ -124,28 +125,28 @@ class AddPackage with Api {
); );
}, },
), ),
_TextParam( TextParam(
'Google play id', 'Google play id',
cardPackDto.googlePlayId, cardPackDto.googlePlayId,
(v) { (v) {
cardPackDto = cardPackDto.copyWith.googlePlayId(v); cardPackDto = cardPackDto.copyWith.googlePlayId(v);
}, },
), ),
_TextParam( TextParam(
'Rustore id', 'Rustore id',
cardPackDto.rustoreId, cardPackDto.rustoreId,
(v) { (v) {
cardPackDto = cardPackDto.copyWith.rustoreId(v); cardPackDto = cardPackDto.copyWith.rustoreId(v);
}, },
), ),
_TextParam( TextParam(
'Appstore id', 'Appstore id',
cardPackDto.appStoreId, cardPackDto.appStoreId,
(v) { (v) {
cardPackDto = cardPackDto.copyWith.appStoreId(v); cardPackDto = cardPackDto.copyWith.appStoreId(v);
}, },
), ),
_TextParam( TextParam(
'Price', 'Price',
cardPackDto.price, cardPackDto.price,
(v) { (v) {
@ -254,7 +255,7 @@ class AddPackage with Api {
), ),
); );
}), }),
_TextParam( TextParam(
'Preview cards', 'Preview cards',
cardPackDto.previewCards?.toString(), cardPackDto.previewCards?.toString(),
(v) { (v) {
@ -265,7 +266,7 @@ class AddPackage with Api {
cardPackDto = cardPackDto.copyWith.previewCards(cards); cardPackDto = cardPackDto.copyWith.previewCards(cards);
}, },
), ),
_TextParam( TextParam(
'Order (lower is higher)', 'Order (lower is higher)',
cardPackDto.order?.toString(), cardPackDto.order?.toString(),
(v) { (v) {
@ -274,7 +275,7 @@ class AddPackage with Api {
); );
}, },
), ),
_TextParam( TextParam(
'Cards order (ids)', 'Cards order (ids)',
cardPackDto.cardsOrder?.join(','), cardPackDto.cardsOrder?.join(','),
(v) { (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,
),
),
],
),
);
}
}

335
lib/admin/add_plan.dart Normal file
View file

@ -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<SubscriptionPlanAdminDto> allPlans = [];
String? planId = null;
List<SubscriptionFeatureEnum> selectedFeatures = [];
late SubscriptionPlanAdminDto planDto;
late AdminApi adminApi;
final availableFeatures = [
SubscriptionFeatureEnum.ads,
SubscriptionFeatureEnum.packs,
];
AddPlan() {
this.adminApi = getIt.get<AdminApi>();
_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<bool> _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<String?>(
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: [],
);
}
}

View file

@ -111,4 +111,29 @@ class AdminApi with Api {
Future<List<CardPackPreviewDto>> packs() => Future<List<CardPackPreviewDto>> packs() =>
getIt.get<PacksApi>().getPacksPreviews(null); getIt.get<PacksApi>().getPacksPreviews(null);
Future<List<SubscriptionPlanAdminDto>> getSubscriptionPlans() async {
final r = await _dio.get<String>(
'$path/subscriptions',
);
return (jsonDecode(r.data!)['plans'] as List)
.cast<Map<String, Object?>>()
.map(SubscriptionPlanAdminDto.fromJson)
.toList();
}
Future<bool> addSubscriptionPlan(SubscriptionPlanAdminDto dto) async {
final r = await _dio.post<String>(
'$path/subscription/add',
data: jsonEncode(dto.toJson()),
);
return r.statusCode == 200;
}
Future<bool> deleteSubscriptionPlan(String id) async {
final r = await _dio.post<String>(
'$path/subscription/delete/$id',
);
return r.statusCode == 200;
}
} }

28
lib/admin/text_param.dart Normal file
View file

@ -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,
),
),
],
),
);
}
}

View file

@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart';
import 'package:mnemo_cards/admin/admin_api.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/pack_cache_manager.dart';
import 'package:mnemo_cards/features/packs/packs_api.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/flags.dart';
import 'package:mnemo_cards/managers/favorite_cards.dart'; import 'package:mnemo_cards/managers/favorite_cards.dart';
import 'package:mnemo_cards/managers/repository/dio_provider.dart'; import 'package:mnemo_cards/managers/repository/dio_provider.dart';
@ -35,6 +36,11 @@ Future<void> setInjections() async {
); );
print('set pack'); print('set pack');
getIt.registerLazySingleton<SubscriptionApi>(
() => SubscriptionApi(getIt.get<DioProvider>().dio),
);
print('set subscription');
if (ADMIN_BUILD) { if (ADMIN_BUILD) {
getIt.registerLazySingleton<AdminApi>( getIt.registerLazySingleton<AdminApi>(
() => AdminApi(getIt.get<DioProvider>().dio)); () => AdminApi(getIt.get<DioProvider>().dio));

View file

@ -9,6 +9,7 @@ import '../features/packs/pack_cache_manager.dart';
import '../features/packs/pack_updater.dart'; import '../features/packs/pack_updater.dart';
import '../features/purchase/in_app_purchase.dart'; import '../features/purchase/in_app_purchase.dart';
import '../features/packs/pack_manager.dart'; import '../features/packs/pack_manager.dart';
import '../features/subscription/subscription_api.dart';
import '../features/tests/test_manager.dart'; import '../features/tests/test_manager.dart';
import '../managers/user_manager.dart'; import '../managers/user_manager.dart';
import 'injector.dart'; import 'injector.dart';
@ -26,6 +27,8 @@ class Locator {
UserManager get userManager => _getIt.get<UserManager>(); UserManager get userManager => _getIt.get<UserManager>();
SubscriptionApi get subscriptionApi => _getIt.get<SubscriptionApi>();
FavoriteCardsController get favoriteCardsController => FavoriteCardsController get favoriteCardsController =>
_getIt.get<FavoriteCardsController>(); _getIt.get<FavoriteCardsController>();

View file

@ -1,12 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards/theme/themes.dart'; import 'package:mnemo_cards/theme/themes.dart';
import '../../main.dart'; import '../../main.dart';
showErrorDialog(String message) { Future<void> showErrorDialog(String message, {Completer? completer}) {
return showDialog( final context = scaffoldKey.currentContext!;
context: scaffoldKey.currentContext!, completer ??= Completer();
final result = showDialog(
context: context,
useRootNavigator: false,
builder: (context) { builder: (context) {
return Center( return Center(
child: Material( child: Material(
@ -56,5 +61,13 @@ showErrorDialog(String message) {
), ),
), ),
); );
},
);
bool dialogClosed = false;
completer.future.then((_) {
if (!dialogClosed) {
return Navigator.of(context).pop();
}
}); });
return result.then((_) => dialogClosed = true);
} }

View file

@ -33,7 +33,7 @@ class PackCacheManager {
await file.writeAsString(jsonEncode(json)); await file.writeAsString(jsonEncode(json));
} }
} catch (e, s) { } 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( Analytics.crashlyticsError(
e: e, e: e,
s: s, s: s,

View file

@ -2,14 +2,14 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:archive/archive_io.dart' as a; import 'package:archive/archive_io.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/widgets.dart';
import 'package:mnemo_cards/features/packs/packs_api.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import '../../di/locator.dart'; import '../analytics/analytics.dart';
import '../../features/packs/pack_cache_manager.dart'; import 'pack_cache_manager.dart';
import '../../features/packs/pack_holder.dart'; import 'pack_holder.dart';
import 'packs_api.dart';
import 'preview_pack_holder.dart'; import 'preview_pack_holder.dart';
/// Updates cache and state holders for available packs /// Updates cache and state holders for available packs
@ -33,13 +33,17 @@ class PackUpdater {
final pollStream = Stream.periodic(const Duration(seconds: 600)); final pollStream = Stream.periodic(const Duration(seconds: 600));
pollStream.listen((event) async { pollStream.listen((event) async {
await updateAvailablePacks(); await updateAvailablePacks();
await _updatePreviewPacks();
});
}
Future<void> _updatePreviewPacks() async {
try { try {
final r = await _packsApi.getPacksPreviews(null); final r = await _packsApi.getPacksPreviews(null);
_previewPackHolder.setPreviewPacks(r, save: true); _previewPackHolder.setPreviewPacks(r, save: true);
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
} }
});
} }
Future<void> updateAvailablePacks() async { Future<void> updateAvailablePacks() async {
@ -106,8 +110,10 @@ class PackUpdater {
} }
} }
Future<CardPackDto> updatePackAndSaveImages(String packId, Future<CardPackDto> updatePackAndSaveImages(
{bool shouldUpdateHolder = false}) async { String packId, {
bool shouldUpdateHolder = false,
}) async {
try { try {
log('Updating with cards ${packId}', name: 'updatePackWithCards'); log('Updating with cards ${packId}', name: 'updatePackWithCards');
final s = Stopwatch()..start(); final s = Stopwatch()..start();
@ -118,14 +124,13 @@ class PackUpdater {
} }
final imagesData = await _packsApi.packImagesArchive(packId); final imagesData = await _packsApi.packImagesArchive(packId);
print('get cards from api ${s.elapsedMilliseconds} mills'); print('get cards from api ${s.elapsedMilliseconds} mills');
final userId = locator.userManager.userId!;
final appVersion = (await PackageInfo.fromPlatform()).version; final appVersion = (await PackageInfo.fromPlatform()).version;
final password = TokenGenerator.generateArchivePassword( final password = TokenGenerator.generateArchivePassword(
appVersion: appVersion, appVersion: appVersion,
packId: packId, packId: packId,
); );
print('generate token ${s.elapsedMilliseconds} mills'); print('generate token ${s.elapsedMilliseconds} mills');
final imagesArchive = a.ZipDecoder().decodeBytes( final imagesArchive = ZipDecoder().decodeBytes(
imagesData, imagesData,
password: password, password: password,
); );
@ -133,7 +138,7 @@ class PackUpdater {
Map<String, MemoryImage> images = {}; Map<String, MemoryImage> images = {};
// todo optimize, make really async // todo optimize, make really async
Future<void> setImage(a.ArchiveFile file) async { Future<void> setImage(ArchiveFile file) async {
final fileData = file.content as List<int>; final fileData = file.content as List<int>;
final data = Uint8List.fromList(fileData); final data = Uint8List.fromList(fileData);
images[file.name] = MemoryImage(data); images[file.name] = MemoryImage(data);
@ -143,7 +148,6 @@ class PackUpdater {
for (final file in imagesArchive) setImage(file), for (final file in imagesArchive) setImage(file),
]); ]);
print('loaded images ${s.elapsedMilliseconds} mills'); print('loaded images ${s.elapsedMilliseconds} mills');
_cacheManager.savePack(packDto, images: images); _cacheManager.savePack(packDto, images: images);
print('saved pack ${s.elapsedMilliseconds} mills'); print('saved pack ${s.elapsedMilliseconds} mills');
@ -156,8 +160,12 @@ class PackUpdater {
} }
return packDto; return packDto;
} catch (e, s) { } catch (e, s) {
log('Cant update pack', name: 'updatePackWithCards'); log('Cant update pack', error: e, stackTrace: s);
log(e.toString(), stackTrace: s); Analytics.crashlyticsError(
message: 'Cant update pack',
e: e,
s: s,
);
rethrow; rethrow;
} }
} }

View file

@ -185,9 +185,9 @@ class PurchaseDetailsStreamSubscription {
}, },
); );
final result = await locator.repository.checkPayment( final result = await locator.repository.checkPayment(
purchaseDetails.productID, itemId: purchaseDetails.productID,
purchaseDetails.verificationData.serverVerificationData, key: purchaseDetails.verificationData.serverVerificationData,
androidPaymentSystem, paymentSystem: androidPaymentSystem,
); );
if (result) { if (result) {
Analytics.buyComplete( Analytics.buyComplete(

View file

@ -54,9 +54,9 @@ class RustorePurchaseService {
// creating payment to check it later // creating payment to check it later
// in case there will be an error after confirmation // in case there will be an error after confirmation
await locator.repository.checkPayment( await locator.repository.checkPayment(
result.successPurchase!.productId, itemId: result.successPurchase!.productId,
result.successPurchase!.subscriptionToken!, key: result.successPurchase!.subscriptionToken!,
androidPaymentSystem, paymentSystem: androidPaymentSystem,
); );
} catch (e, s) { } catch (e, s) {
Analytics.buyError( Analytics.buyError(
@ -70,13 +70,13 @@ class RustorePurchaseService {
return false; return false;
} }
final p = await RustoreBillingClient.confirm( final p = await RustoreBillingClient.confirm(
result.successPurchase!.purchaseId, result.successPurchase!.subscriptionToken!,
); );
if (p.success) { if (p.success) {
final r = await locator.repository.checkPayment( final r = await locator.repository.checkPayment(
result.successPurchase!.productId, itemId: result.successPurchase!.productId,
result.successPurchase!.subscriptionToken!, key: result.successPurchase!.subscriptionToken!,
androidPaymentSystem, paymentSystem: androidPaymentSystem,
); );
if (r) { if (r) {
return r; return r;

View file

@ -10,16 +10,15 @@ class SubscriptionApi with Api {
SubscriptionApi(this._dio); SubscriptionApi(this._dio);
// buy page dto if not available Future<SubscriptionDto?> getBuySubscription() async {
Future<CardPackBuyDto?> getPackBuy(String id) async {
final r = await _dio.get<String>( final r = await _dio.get<String>(
'$path/pack/buy/$id', '$path/subscription/page',
options: Options( options: Options(
sendTimeout: Duration(seconds: 5), sendTimeout: Duration(seconds: 5),
receiveTimeout: 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 /// cards zip archive for available pack

View file

@ -1,9 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:mnemo_cards/domain/router/dialogs.dart';
import 'package:yandex_mobileads/mobile_ads.dart'; import 'package:yandex_mobileads/mobile_ads.dart';
import '../../main.dart';
class YandexAds { class YandexAds {
static BannerAd createBanner( static BannerAd createBanner(
BoxConstraints constraints, { BoxConstraints constraints, {
@ -42,6 +46,7 @@ class YandexAds {
} }
static RewardedAd? _preloadedAd; static RewardedAd? _preloadedAd;
static Completer? _showingAdCompleter;
static Future<RewardedAd?> loadRewardedAd() async { static Future<RewardedAd?> loadRewardedAd() async {
final completer = Completer<RewardedAd?>(); final completer = Completer<RewardedAd?>();
@ -66,30 +71,58 @@ class YandexAds {
Function(Reward reward) onRewarded, { Function(Reward reward) onRewarded, {
maxAttempts = 2, maxAttempts = 2,
}) async { }) async {
if (_showingAdCompleter?.isCompleted == false) {
return false;
}
try {
_showingAdCompleter = Completer();
if (_preloadedAd == null) { if (_preloadedAd == null) {
final _loadedAd = await loadRewardedAd(); // final dialogCompleter = Completer();
// final dialogFuture =
// showErrorDialog('Загрузка...', completer: dialogCompleter);
try {
final _loadedAd = await loadRewardedAd().timeout(
Duration(seconds: 5),
);
if (_loadedAd == null) { if (_loadedAd == null) {
return false; return false;
} }
} finally {}
// dialogCompleter.complete();
// await dialogFuture;
// final frameCompleter = Completer();
// WidgetsBinding.instance.addPostFrameCallback((_) {
// frameCompleter.complete();
// });
// await frameCompleter;
} }
final _ad = _preloadedAd; final _ad = _preloadedAd;
_preloadedAd = null; _preloadedAd = null;
_ad?.setAdEventListener( await _ad?.setAdEventListener(
eventListener: RewardedAdEventListener(onAdDismissed: () { eventListener: RewardedAdEventListener(
onAdDismissed: () {
_ad.destroy(); _ad.destroy();
}, onAdFailedToShow: (e) { },
onAdFailedToShow: (e) {
_ad.destroy(); _ad.destroy();
if (maxAttempts-- > 0) { if (maxAttempts-- > 0) {
showRewardedAd(onRewarded, maxAttempts: maxAttempts); showRewardedAd(onRewarded, maxAttempts: maxAttempts);
} }
}, onAdShown: () { },
onAdShown: () {
// _ad.destroy(); // _ad.destroy();
}, onRewarded: (r) { },
onRewarded: (r) {
onRewarded(r); onRewarded(r);
_ad.destroy(); _ad.destroy();
})); },
),
);
await _ad?.show(); await _ad?.show();
final reward = await _ad?.waitForDismiss(); final reward = await _ad?.waitForDismiss();
return reward != null; return reward != null;
} finally {
_showingAdCompleter?.complete();
}
} }
} }

View file

@ -9,7 +9,7 @@ mixin Api {
? 'https://89.19.210.178:8080' //FirebaseRepository.appConfig.path ? 'https://89.19.210.178:8080' //FirebaseRepository.appConfig.path
: androidDeviceInfo!.isPhysicalDevice : androidDeviceInfo!.isPhysicalDevice
//device to localhost //device to localhost
? 'https://192.168.31.158:8080' ? 'https://192.168.31.159:8080'
: //emulator to localhost : //emulator to localhost
'https://10.0.2.2:8080'; 'https://10.0.2.2:8080';
} }

View file

@ -119,11 +119,11 @@ class HttpRepository extends Repository with Api {
} }
@override @override
Future<bool> checkPayment( Future<bool> checkPayment({
String itemId, required String itemId,
String key, required String key,
PaymentSystem paymentSystem, required PaymentSystem paymentSystem,
) async { }) async {
final r = await _dio.get<String>( final r = await _dio.get<String>(
'$path/check_payment/', '$path/check_payment/',
queryParameters: { queryParameters: {

View file

@ -31,11 +31,11 @@ abstract class Repository {
Future<TestDto?> getTest(String id); Future<TestDto?> getTest(String id);
Future<bool> checkPayment( Future<bool> checkPayment({
String itemId, required String itemId,
String key, required String key,
PaymentSystem PaymentSystem, required PaymentSystem paymentSystem,
); });
Future<bool> checkUserPayment(); Future<bool> checkUserPayment();

View file

@ -34,7 +34,9 @@ class UserManager {
UserManager(this._repository) : userStateHolder = UserStateHolder(); 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; int? get userId => userStateHolder.user?.id;

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.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.dart';
import 'package:mnemo_cards/domain/router/app_router.gr.dart'; import 'package:mnemo_cards/domain/router/app_router.gr.dart';
import 'package:mnemo_cards/features/yandex_ads/yandex_ads.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 '../domain/router/dialogs.dart';
import '../features/analytics/analytics.dart'; import '../features/analytics/analytics.dart';
import '../theme/themes.dart'; import '../theme/themes.dart';
import '../widgets/buy_subscription_widget.dart';
import '../widgets/enter_promo_code_widget.dart'; import '../widgets/enter_promo_code_widget.dart';
import '../widgets/shared_pref_button.dart'; import '../widgets/shared_pref_button.dart';
@ -75,7 +77,7 @@ class ProfilePage extends StatelessWidget {
context: context, context: context,
builder: (c) => const AllCards()); builder: (c) => const AllCards());
}, },
child: Text('ALL CARDS'), child: Text('Cards'),
), ),
), ),
Expanded( Expanded(
@ -85,7 +87,15 @@ class ProfilePage extends StatelessWidget {
context: context, context: context,
builder: (c) => const AllUsers()); 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, height: 5,
color: borderGray.withOpacity(0.2), color: borderGray.withOpacity(0.2),
), ),
const MobileBuySubscriptionWidget(),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
if (SHOW_ADMIN) if (SHOW_ADMIN)
SimpleTile(title: Text('Посмотреть рекламу'), onTap: () { SimpleTile(title: Text('Посмотреть рекламу'), onTap: () {
YandexAds.showRewardedAd((r) { YandexAds.showRewardedAd((r) {

View file

@ -1,14 +1,20 @@
import 'dart:math';
import 'package:flutter/cupertino.dart'; 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/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 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
import '../di/injector.dart'; import '../di/injector.dart';
import '../di/locator.dart'; import '../di/locator.dart';
import '../domain/router/app_router.dart'; import '../domain/router/app_router.dart';
import '../features/packs/packs_api.dart'; import '../features/packs/packs_api.dart';
import '../theme/themes.dart';
class MobileBuySubscriptionWidget extends StatelessWidget { class MobileBuySubscriptionWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BuySubscriptionWidget(onBuySubscription: onBuySubscription); return BuySubscriptionWidget(onBuySubscription: onBuySubscription);
@ -20,12 +26,188 @@ class MobileBuySubscriptionWidget extends StatelessWidget {
AppRouter.openAuthOrProfile(); AppRouter.openAuthOrProfile();
return; return;
} }
if (user.subscription) { final api = locator.subscriptionApi;
return; final buyDto = await api.getBuySubscription().catchError((_) => null);
if (buyDto != null) {
showDialog(
context: scaffoldKey.currentContext!,
builder: (_) => SubscriptionPage(buyDto.page!),
);
} }
final packsApi = getIt.get<PacksApi>();
// final buyDto = await packsApi.getPackBuy(id).catchError((_) => null);
} }
const MobileBuySubscriptionWidget(); const MobileBuySubscriptionWidget();
} }
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();
}
}

View file

@ -127,7 +127,7 @@ class AvailablePack extends StatelessWidget {
], ],
); );
}), }),
_AdTile(), if (locator.userManager.user?.subscription != true) _AdTile(),
_TestsSection(cardPack.id.toString()), _TestsSection(cardPack.id.toString()),
], ],
), ),
@ -258,8 +258,7 @@ class _AdTile extends StatelessWidget {
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
final banner = YandexAds.createBanner( final banner = YandexAds.createBanner(
BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)), BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)),
id: 'R-M-3761037-2' id: 'R-M-3761037-2');
);
return FadeInWidget( return FadeInWidget(
future: banner.loadAd(), future: banner.loadAd(),
animationDuration: Duration(milliseconds: 1000), animationDuration: Duration(milliseconds: 1000),

View file

@ -5,9 +5,9 @@ import 'package:mnemo_cards/utils/string_helper.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.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 '../../features/analytics/analytics.dart';
import '../../main.dart'; import '../../main.dart';
import '../items/builder.dart';
import '../../di/locator.dart'; import '../../di/locator.dart';
import '../../domain/router/app_router.dart'; import '../../domain/router/app_router.dart';

View file

@ -61,9 +61,7 @@ class GameCardDecoration extends StatelessWidget {
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
// color: Theme.of(context).scaffoldBackgroundColor, // color: Theme.of(context).scaffoldBackgroundColor,
color: Theme.of(context) color: Theme.of(context).scaffoldBackgroundColor.lighten(0.05),
.scaffoldBackgroundColor
.lighten(0.05),
// .withOpacity(alpha), // .withOpacity(alpha),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@ -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(),
);
});
}
}

View file

@ -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<Item> {
Iterable<Widget> 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,
),
)
],
),
);
}
}

View file

@ -576,6 +576,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: freezed:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -59,6 +59,7 @@ dependencies:
flutter_colorpicker: ^1.1.0 flutter_colorpicker: ^1.1.0
jailbreak_root_detection: ^1.1.1 jailbreak_root_detection: ^1.1.1
no_screenshot: ^0.0.1+6 no_screenshot: ^0.0.1+6
flutter_widget_from_html_core: ^0.15.1
file_picker: file_picker:
url_launcher: ^6.2.6 url_launcher: ^6.2.6