sub
This commit is contained in:
parent
5a37539554
commit
35725aeaa1
25 changed files with 800 additions and 280 deletions
|
|
@ -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
335
lib/admin/add_plan.dart
Normal 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: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
28
lib/admin/text_param.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,73 @@
|
||||||
|
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();
|
||||||
builder: (context) {
|
final result = showDialog(
|
||||||
return Center(
|
context: context,
|
||||||
child: Material(
|
useRootNavigator: false,
|
||||||
color: Colors.transparent,
|
builder: (context) {
|
||||||
child: Container(
|
return Center(
|
||||||
margin: EdgeInsets.all(4.0.h),
|
child: Material(
|
||||||
padding: EdgeInsets.all(4.0.h),
|
color: Colors.transparent,
|
||||||
height: 200.h,
|
child: Container(
|
||||||
width: MediaQuery.of(context).size.width * 0.8,
|
margin: EdgeInsets.all(4.0.h),
|
||||||
decoration: BoxDecoration(
|
padding: EdgeInsets.all(4.0.h),
|
||||||
color: white,
|
height: 200.h,
|
||||||
border: Border.all(
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
color: Colors.grey.withOpacity(0.5),
|
decoration: BoxDecoration(
|
||||||
width: 1,
|
color: white,
|
||||||
),
|
border: Border.all(
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
color: Colors.grey.withOpacity(0.5),
|
||||||
|
width: 1,
|
||||||
),
|
),
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
),
|
||||||
children: [
|
child: Column(
|
||||||
Expanded(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Center(
|
children: [
|
||||||
child: Text(
|
Expanded(
|
||||||
message,
|
child: Center(
|
||||||
textAlign: TextAlign.center,
|
child: Text(
|
||||||
),
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
),
|
||||||
onTap: Navigator.of(context).pop,
|
GestureDetector(
|
||||||
child: Container(
|
onTap: Navigator.of(context).pop,
|
||||||
height: 40.h,
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
height: 40.h,
|
||||||
color: testBlue,
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
color: testBlue,
|
||||||
color: Colors.grey.withOpacity(0.5),
|
border: Border.all(
|
||||||
width: 1,
|
color: Colors.grey.withOpacity(0.5),
|
||||||
),
|
width: 1,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
child: Text('Оки'),
|
|
||||||
),
|
),
|
||||||
)
|
alignment: Alignment.center,
|
||||||
],
|
child: Text('Оки'),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
bool dialogClosed = false;
|
||||||
|
completer.future.then((_) {
|
||||||
|
if (!dialogClosed) {
|
||||||
|
return Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result.then((_) => dialogClosed = true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,15 +33,19 @@ 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();
|
||||||
try {
|
await _updatePreviewPacks();
|
||||||
final r = await _packsApi.getPacksPreviews(null);
|
|
||||||
_previewPackHolder.setPreviewPacks(r, save: true);
|
|
||||||
} catch (e) {
|
|
||||||
log(e.toString());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _updatePreviewPacks() async {
|
||||||
|
try {
|
||||||
|
final r = await _packsApi.getPacksPreviews(null);
|
||||||
|
_previewPackHolder.setPreviewPacks(r, save: true);
|
||||||
|
} catch (e) {
|
||||||
|
log(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateAvailablePacks() async {
|
Future<void> updateAvailablePacks() async {
|
||||||
if (_updateCompleter?.isCompleted ?? true) {
|
if (_updateCompleter?.isCompleted ?? true) {
|
||||||
_updateCompleter = Completer();
|
_updateCompleter = Completer();
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 (_preloadedAd == null) {
|
if (_showingAdCompleter?.isCompleted == false) {
|
||||||
final _loadedAd = await loadRewardedAd();
|
return false;
|
||||||
if (_loadedAd == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
final _ad = _preloadedAd;
|
try {
|
||||||
_preloadedAd = null;
|
_showingAdCompleter = Completer();
|
||||||
_ad?.setAdEventListener(
|
if (_preloadedAd == null) {
|
||||||
eventListener: RewardedAdEventListener(onAdDismissed: () {
|
// final dialogCompleter = Completer();
|
||||||
_ad.destroy();
|
// final dialogFuture =
|
||||||
}, onAdFailedToShow: (e) {
|
// showErrorDialog('Загрузка...', completer: dialogCompleter);
|
||||||
_ad.destroy();
|
try {
|
||||||
if (maxAttempts-- > 0) {
|
final _loadedAd = await loadRewardedAd().timeout(
|
||||||
showRewardedAd(onRewarded, maxAttempts: maxAttempts);
|
Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
if (_loadedAd == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} finally {}
|
||||||
|
// dialogCompleter.complete();
|
||||||
|
// await dialogFuture;
|
||||||
|
// final frameCompleter = Completer();
|
||||||
|
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// frameCompleter.complete();
|
||||||
|
// });
|
||||||
|
// await frameCompleter;
|
||||||
}
|
}
|
||||||
}, onAdShown: () {
|
final _ad = _preloadedAd;
|
||||||
// _ad.destroy();
|
_preloadedAd = null;
|
||||||
}, onRewarded: (r) {
|
await _ad?.setAdEventListener(
|
||||||
onRewarded(r);
|
eventListener: RewardedAdEventListener(
|
||||||
_ad.destroy();
|
onAdDismissed: () {
|
||||||
}));
|
_ad.destroy();
|
||||||
await _ad?.show();
|
},
|
||||||
final reward = await _ad?.waitForDismiss();
|
onAdFailedToShow: (e) {
|
||||||
return reward != null;
|
_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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ class AvailablePack extends StatelessWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
_AdTile(),
|
if (locator.userManager.user?.subscription != true) _AdTile(),
|
||||||
_TestsSection(cardPack.id.toString()),
|
_TestsSection(cardPack.id.toString()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -257,9 +257,8 @@ class _AdTile extends StatelessWidget {
|
||||||
if (kIsWeb) return SizedBox.shrink();
|
if (kIsWeb) return SizedBox.shrink();
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,8 @@ 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
|
// .withOpacity(alpha),
|
||||||
.lighten(0.05),
|
|
||||||
// .withOpacity(alpha),
|
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Theme.of(context)
|
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(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue