some fixes

This commit is contained in:
Dmitry 2024-08-07 00:33:11 +03:00
parent 67cfa49e2d
commit d5052aff77
13 changed files with 239 additions and 62 deletions

View file

@ -1,33 +1,43 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:label="Mnemo cards"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/launcher_icon"
android:label="Mnemo cards">
<meta-data <meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID" android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/> android:value="ca-app-pub-3940256099942544~3347511713" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme" />
/>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mnemocards" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
@ -43,8 +53,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </queries>
</manifest> </manifest>

View file

@ -1,9 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:mnemo_cards/admin/admin_api.dart'; import 'package:mnemo_cards/admin/admin_api.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 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
@ -14,7 +10,6 @@ import 'text_param.dart';
class AddPlan { class AddPlan {
List<SubscriptionPlanAdminDto> allPlans = []; List<SubscriptionPlanAdminDto> allPlans = [];
String? planId = null;
List<SubscriptionFeatureEnum> selectedFeatures = []; List<SubscriptionFeatureEnum> selectedFeatures = [];
late SubscriptionPlanAdminDto planDto; late SubscriptionPlanAdminDto planDto;
late AdminApi adminApi; late AdminApi adminApi;
@ -30,7 +25,6 @@ class AddPlan {
} }
void _init(SubscriptionPlanAdminDto? dto) { void _init(SubscriptionPlanAdminDto? dto) {
planId = dto?.id;
planDto = dto ?? planDto = dto ??
const SubscriptionPlanAdminDto( const SubscriptionPlanAdminDto(
ui: SubscriptionPlanUIDto( ui: SubscriptionPlanUIDto(
@ -43,6 +37,8 @@ class AddPlan {
SubscriptionFeatureEnum.ads, SubscriptionFeatureEnum.ads,
SubscriptionFeatureEnum.packs, SubscriptionFeatureEnum.packs,
], ],
paymentId: null,
paymentSystem: PaymentSystem.unknown,
); );
selectedFeatures = planDto.features.toList(); selectedFeatures = planDto.features.toList();
} }
@ -122,6 +118,14 @@ class AddPlan {
onPressed: _saveSubscriptionPlan, onPressed: _saveSubscriptionPlan,
icon: Icon(Icons.save), icon: Icon(Icons.save),
), ),
if (planDto.id != null)
IconButton(
onPressed: () async {
planDto = planDto.copyWith(id: null);
_saveSubscriptionPlan();
},
icon: Icon(Icons.copy),
),
if (planDto.id != null) if (planDto.id != null)
GestureDetector( GestureDetector(
onLongPress: _deletePLan, onLongPress: _deletePLan,
@ -309,6 +313,56 @@ class AddPlan {
planDto.copyWith.ui(ui); planDto.copyWith.ui(ui);
}, },
), ),
Padding(
padding:
const EdgeInsets.all(8.0),
child: Row(
children: [
Text('Payment type '),
Expanded(
child: StatefulBuilder(
builder: (context,
setState) {
return DropdownButton<
PaymentSystem>(
value: planDto
.paymentSystem,
items:
PaymentSystem.values
.map(
(v) =>
DropdownMenuItem(
child: Text(
v.name),
value: v,
),
)
.toList(),
onChanged: (v) {
setState(() {
planDto = planDto
.copyWith(
paymentSystem: v ??
PaymentSystem
.unknown,
);
});
},
);
}),
),
],
),
),
TextParam(
'Payment id',
planDto.paymentId,
(v) {
planDto = planDto.copyWith(
paymentId: v,
);
},
),
], ],
), ),
], ],

View file

@ -15,6 +15,7 @@ class TextParam extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Text(title), Text(title),
SizedBox(width: 2,),
Expanded( Expanded(
child: TextField( child: TextField(
controller: TextEditingController(text: initial), controller: TextEditingController(text: initial),

View file

@ -41,12 +41,6 @@ Future<void> setInjections() async {
); );
print('set subscription'); print('set subscription');
if (ADMIN_BUILD) {
getIt.registerLazySingleton<AdminApi>(
() => AdminApi(getIt.get<DioProvider>().dio));
print('set admin');
}
getIt.registerLazySingleton<PackHolder>( getIt.registerLazySingleton<PackHolder>(
() => PackHolder(), () => PackHolder(),
); );

View file

@ -96,6 +96,25 @@ class Analytics {
}.asFirebaseMap); }.asFirebaseMap);
} }
static Future<void> buySubscriptionError(
SubscriptionPlanDto? dto,
PaymentSystem system, {
Map<String, String?>? data,
String? message,
}) {
return _analytics.logEvent(
name: 'buy_sub_error',
parameters: {
'id': dto?.id.toString(),
'system': system.name,
'currency': dto?.currency,
'price': dto?.price.toString(),
if (message != null) 'message': message,
if (data != null) ...data,
}.asFirebaseMap,
);
}
static Future<void> buyEvent( static Future<void> buyEvent(
CardPackBuyDto? dto, CardPackBuyDto? dto,
PaymentSystem system, { PaymentSystem system, {

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
@ -27,6 +28,36 @@ class InAppPurchaseService {
InAppPurchase get instance => _inAppPurchase; InAppPurchase get instance => _inAppPurchase;
Future<bool> buySubscription(SubscriptionPlanDto dto) async {
if (dto.paymentSystem == PaymentSystem.yookassa) {
return false;
}
final subscriptionProducts = await _getSubscriptionStoreProduct(dto);
ProductDetails? product;
if (Platform.isAndroid && !RUSTORE_BUILD) {
// final googlePlayProducts = subscriptionProducts.whereType<GooglePlayProductDetails>();
product = subscriptionProducts.firstWhereOrNull(
(p) => p.rawPrice == double.parse(dto.price),
);
} else {
product = subscriptionProducts.first;
}
if (product == null) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
message: 'cant find plan with price ${dto.price}',
data: {
'plans': subscriptionProducts.join(';'),
},
);
return false;
}
final result = await _buySubscriptionInStore(product);
return result;
}
Future<bool> buyPack(CardPackBuyDto dto) async { Future<bool> buyPack(CardPackBuyDto dto) async {
try { try {
if (RUSTORE_BUILD) { if (RUSTORE_BUILD) {
@ -73,11 +104,8 @@ class InAppPurchaseService {
if (url.startsWith('http')) { if (url.startsWith('http')) {
return NavigationDecision.navigate; return NavigationDecision.navigate;
} else { } else {
if (await canLaunchUrlString(url)) {
launchUrlString(url); launchUrlString(url);
return NavigationDecision.prevent; return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
} }
}, },
), ),
@ -127,7 +155,7 @@ class InAppPurchaseService {
return []; return [];
} }
final ids = { final ids = {
if (Platform.isAndroid) dto.googlePlayId, if (Platform.isAndroid) RUSTORE_BUILD ? dto.rustoreId : dto.googlePlayId,
if (Platform.isIOS) dto.appStoreId, if (Platform.isIOS) dto.appStoreId,
}.whereNotNull().toSet(); }.whereNotNull().toSet();
if (ids.isEmpty) { if (ids.isEmpty) {
@ -158,7 +186,60 @@ class InAppPurchaseService {
return productDetailResponse.productDetails; return productDetailResponse.productDetails;
} }
Future<List<ProductDetails>> _getSubscriptionStoreProduct(
SubscriptionPlanDto dto,
) async {
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
message: 'in app purchase not available',
);
return [];
}
final ids = {
if (Platform.isAndroid)
if (RUSTORE_BUILD && dto.paymentSystem == PaymentSystem.rustore)
dto.paymentId,
if (!RUSTORE_BUILD && dto.paymentSystem == PaymentSystem.google)
dto.googlePlaySubscriptionId,
if (dto.paymentSystem == PaymentSystem.yookassa) dto.paymentId,
if (Platform.isIOS && dto.paymentSystem == PaymentSystem.apple)
dto.paymentId,
}.whereNotNull().toSet();
if (ids.isEmpty) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
message: 'product id not set',
);
return [];
}
final productDetailResponse = await _inAppPurchase.queryProductDetails(ids);
if (productDetailResponse.error != null ||
productDetailResponse.productDetails.isEmpty) {
Analytics.buySubscriptionError(
dto,
androidPaymentSystem,
data: {
'msg': 'cant get product details ${productDetailResponse.error}',
'not_found': productDetailResponse.notFoundIDs.join(','),
},
);
return [];
}
return productDetailResponse.productDetails;
}
Future<bool> _buyItemInStore(ProductDetails product) async { Future<bool> _buyItemInStore(ProductDetails product) async {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
return InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
}
Future<bool> _buySubscriptionInStore(ProductDetails product) async {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product); final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
return InAppPurchase.instance return InAppPurchase.instance
.buyNonConsumable(purchaseParam: purchaseParam); .buyNonConsumable(purchaseParam: purchaseParam);

View file

@ -1,11 +1,20 @@
import 'package:mnemo_cards/admin/admin_api.dart';
import 'package:mnemo_cards/di/injector.dart';
import 'package:mnemo_cards/main.dart'; import 'package:mnemo_cards/main.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 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
bool get ADMIN_BUILD => CommonPrefs.ADMIN_BUILD; import 'di/locator.dart';
import 'managers/user_manager.dart';
bool get ADMIN_BUILD =>
getIt.isRegistered<UserManager>() &&
locator.userManager.user?.admin == true;
const RUSTORE_BUILD = CommonPrefs.RUSTORE_BUILD; const RUSTORE_BUILD = CommonPrefs.RUSTORE_BUILD;
final androidPaymentSystem = CommonPrefs.androidPaymentSystem; final androidPaymentSystem = CommonPrefs.androidPaymentSystem;
bool get SHOW_ADMIN => bool get SHOW_ADMIN =>
ADMIN_BUILD && (globalSharedPreferences.getBool('show_admin') ?? true); getIt.isRegistered<AdminApi>() &&
ADMIN_BUILD &&
(globalSharedPreferences.getBool('show_admin') ?? true);

View file

@ -89,7 +89,7 @@ void main() async {
await locator.previewPackPoller.init(); await locator.previewPackPoller.init();
await locator.packUpdater.init(); await locator.packUpdater.init();
print('initing complete ${stopwatch.elapsedMilliseconds}'); print('initing complete ${stopwatch.elapsedMilliseconds}');
if (!kIsWeb) unawaited(MobileAds.initialize()); if (!kIsWeb) unawaited(MobileAds.initialize());
print('mobile ads ${stopwatch.elapsedMilliseconds}'); print('mobile ads ${stopwatch.elapsedMilliseconds}');
print('runApp ${stopwatch.elapsedMilliseconds}'); print('runApp ${stopwatch.elapsedMilliseconds}');
runApp(const MyApp()); runApp(const MyApp());
@ -152,15 +152,9 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
darkTheme: darkTheme2, darkTheme: darkTheme2,
themeMode: themeMode, themeMode: themeMode,
routerConfig: appRouter.config( routerConfig: appRouter.config(
deepLinkBuilder: (link) { deepLinkBuilder: (link) {
// if(link.path.contains('/ru/reset-password')) { return DeepLink.defaultPath;
// return const DeepLink( },
// [ResetPassRoute()]
// );
// } else {
return DeepLink.defaultPath;
}
// }
), ),
scaffoldMessengerKey: scaffoldMessengerKey, scaffoldMessengerKey: scaffoldMessengerKey,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
@ -209,7 +203,6 @@ class _MainTabsPageState extends State<MainTabsPage> {
_Tabs _currentTab = _Tabs.home; _Tabs _currentTab = _Tabs.home;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();

View file

@ -65,7 +65,7 @@ class ProfilePage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (SHOW_ADMIN) if (SHOW_ADMIN) ...[
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -100,7 +100,6 @@ class ProfilePage extends StatelessWidget {
), ),
], ],
), ),
if (SHOW_ADMIN)
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -126,6 +125,7 @@ class ProfilePage extends StatelessWidget {
), ),
], ],
), ),
],
Expanded( Expanded(
child: StreamBuilder( child: StreamBuilder(
stream: locator.userManager.userStateHolder.asStream, stream: locator.userManager.userStateHolder.asStream,
@ -156,22 +156,26 @@ class ProfilePage extends StatelessWidget {
height: 5, height: 5,
color: borderGray.withOpacity(0.2), color: borderGray.withOpacity(0.2),
), ),
const MobileBuySubscriptionWidget(), if (SHOW_ADMIN) ...[
Divider( const MobileBuySubscriptionWidget(),
height: 5, Divider(
color: borderGray.withOpacity(0.2), height: 5,
), color: borderGray.withOpacity(0.2),
if (SHOW_ADMIN) ),
SimpleTile(title: Text('Посмотреть рекламу'), onTap: () { SimpleTile(
YandexAds.showRewardedAd((r) { title: Text('Посмотреть рекламу'),
showErrorDialog('Вы получили\n${r.amount} ${r.type}'); onTap: () {
}); YandexAds.showRewardedAd((r) {
},), showErrorDialog(
if (SHOW_ADMIN) 'Вы получили\n${r.amount} ${r.type}');
Divider( });
height: 5, },
color: borderGray.withOpacity(0.2), ),
), Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
],
SimpleTile.text( SimpleTile.text(
text: 'Написать в телеграм', text: 'Написать в телеграм',
onTap: () => launchUrl( onTap: () => launchUrl(

View file

@ -8,8 +8,11 @@ import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../admin/admin_api.dart';
import '../di/injector.dart';
import '../di/locator.dart'; import '../di/locator.dart';
import '../features/analytics/analytics.dart'; import '../features/analytics/analytics.dart';
import '../managers/repository/dio_provider.dart';
import '../theme/themes.dart'; import '../theme/themes.dart';
import '../widgets/shared_pref_button.dart'; import '../widgets/shared_pref_button.dart';
@ -125,7 +128,15 @@ class SettingsPage extends StatelessWidget {
value: enabled ?? false, value: enabled ?? false,
), ),
), ),
setOnTap: (v) => !(v ?? false), setOnTap: (v) {
final val = !(v ?? false);
if (val && !getIt.isRegistered<AdminApi>()) {
getIt.registerLazySingleton<AdminApi>(
() => AdminApi(getIt.get<DioProvider>().dio));
print('set admin');
}
return val;
},
), ),
], ],
), ),

View file

@ -71,7 +71,7 @@ class _SubscriptionPlanWidget extends StatelessWidget {
height: (90 + (plan.tip == null ? 0 : 13)).h, height: (90 + (plan.tip == null ? 0 : 13)).h,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
locator.purchaseService.buySubscription(plan);
}, },
child: Stack( child: Stack(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,

View file

@ -729,7 +729,7 @@ packages:
source: hosted source: hosted
version: "3.2.0" version: "3.2.0"
in_app_purchase_android: in_app_purchase_android:
dependency: transitive dependency: "direct main"
description: description:
name: in_app_purchase_android name: in_app_purchase_android
sha256: f3f3ded26d08d13383b8c9952d1cf1422f0a3440b04d27f9838adee86f2ce899 sha256: f3f3ded26d08d13383b8c9952d1cf1422f0a3440b04d27f9838adee86f2ce899

View file

@ -46,6 +46,7 @@ dependencies:
firebase_crashlytics: ^3.5.7 firebase_crashlytics: ^3.5.7
firebase_analytics: ^10.10.7 firebase_analytics: ^10.10.7
in_app_purchase: ^3.2.0 in_app_purchase: ^3.2.0
in_app_purchase_android:
dot_navigation_bar: ^1.0.2 dot_navigation_bar: ^1.0.2
reorderables: ^0.6.0 reorderables: ^0.6.0
story: ^1.1.0 story: ^1.1.0