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">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="Mnemo cards"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
android:icon="@mipmap/launcher_icon"
android:label="Mnemo cards">
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
android:value="ca-app-pub-3940256099942544~3347511713" />
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<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>
</activity>
<!-- 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. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>

View file

@ -1,9 +1,5 @@
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';
@ -14,7 +10,6 @@ import 'text_param.dart';
class AddPlan {
List<SubscriptionPlanAdminDto> allPlans = [];
String? planId = null;
List<SubscriptionFeatureEnum> selectedFeatures = [];
late SubscriptionPlanAdminDto planDto;
late AdminApi adminApi;
@ -30,7 +25,6 @@ class AddPlan {
}
void _init(SubscriptionPlanAdminDto? dto) {
planId = dto?.id;
planDto = dto ??
const SubscriptionPlanAdminDto(
ui: SubscriptionPlanUIDto(
@ -43,6 +37,8 @@ class AddPlan {
SubscriptionFeatureEnum.ads,
SubscriptionFeatureEnum.packs,
],
paymentId: null,
paymentSystem: PaymentSystem.unknown,
);
selectedFeatures = planDto.features.toList();
}
@ -122,6 +118,14 @@ class AddPlan {
onPressed: _saveSubscriptionPlan,
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)
GestureDetector(
onLongPress: _deletePLan,
@ -309,6 +313,56 @@ class AddPlan {
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(
children: [
Text(title),
SizedBox(width: 2,),
Expanded(
child: TextField(
controller: TextEditingController(text: initial),

View file

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

View file

@ -96,6 +96,25 @@ class Analytics {
}.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(
CardPackBuyDto? dto,
PaymentSystem system, {

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.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:url_launcher/url_launcher_string.dart';
import 'package:webview_flutter/webview_flutter.dart';
@ -27,6 +28,36 @@ class InAppPurchaseService {
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 {
try {
if (RUSTORE_BUILD) {
@ -73,11 +104,8 @@ class InAppPurchaseService {
if (url.startsWith('http')) {
return NavigationDecision.navigate;
} else {
if (await canLaunchUrlString(url)) {
launchUrlString(url);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
}
},
),
@ -127,7 +155,7 @@ class InAppPurchaseService {
return [];
}
final ids = {
if (Platform.isAndroid) dto.googlePlayId,
if (Platform.isAndroid) RUSTORE_BUILD ? dto.rustoreId : dto.googlePlayId,
if (Platform.isIOS) dto.appStoreId,
}.whereNotNull().toSet();
if (ids.isEmpty) {
@ -158,7 +186,60 @@ class InAppPurchaseService {
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 {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
return InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
}
Future<bool> _buySubscriptionInStore(ProductDetails product) async {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
return InAppPurchase.instance
.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_common/mnemo_cards_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;
final androidPaymentSystem = CommonPrefs.androidPaymentSystem;
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.packUpdater.init();
print('initing complete ${stopwatch.elapsedMilliseconds}');
if (!kIsWeb) unawaited(MobileAds.initialize());
if (!kIsWeb) unawaited(MobileAds.initialize());
print('mobile ads ${stopwatch.elapsedMilliseconds}');
print('runApp ${stopwatch.elapsedMilliseconds}');
runApp(const MyApp());
@ -152,15 +152,9 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
darkTheme: darkTheme2,
themeMode: themeMode,
routerConfig: appRouter.config(
deepLinkBuilder: (link) {
// if(link.path.contains('/ru/reset-password')) {
// return const DeepLink(
// [ResetPassRoute()]
// );
// } else {
return DeepLink.defaultPath;
}
// }
deepLinkBuilder: (link) {
return DeepLink.defaultPath;
},
),
scaffoldMessengerKey: scaffoldMessengerKey,
debugShowCheckedModeBanner: false,
@ -209,7 +203,6 @@ class _MainTabsPageState extends State<MainTabsPage> {
_Tabs _currentTab = _Tabs.home;
@override
void didChangeDependencies() {
super.didChangeDependencies();

View file

@ -65,7 +65,7 @@ class ProfilePage extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (SHOW_ADMIN)
if (SHOW_ADMIN) ...[
Row(
children: [
Expanded(
@ -100,7 +100,6 @@ class ProfilePage extends StatelessWidget {
),
],
),
if (SHOW_ADMIN)
Row(
children: [
Expanded(
@ -126,6 +125,7 @@ class ProfilePage extends StatelessWidget {
),
],
),
],
Expanded(
child: StreamBuilder(
stream: locator.userManager.userStateHolder.asStream,
@ -156,22 +156,26 @@ class ProfilePage extends StatelessWidget {
height: 5,
color: borderGray.withOpacity(0.2),
),
const MobileBuySubscriptionWidget(),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
if (SHOW_ADMIN)
SimpleTile(title: Text('Посмотреть рекламу'), onTap: () {
YandexAds.showRewardedAd((r) {
showErrorDialog('Вы получили\n${r.amount} ${r.type}');
});
},),
if (SHOW_ADMIN)
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
if (SHOW_ADMIN) ...[
const MobileBuySubscriptionWidget(),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
SimpleTile(
title: Text('Посмотреть рекламу'),
onTap: () {
YandexAds.showRewardedAd((r) {
showErrorDialog(
'Вы получили\n${r.amount} ${r.type}');
});
},
),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
],
SimpleTile.text(
text: 'Написать в телеграм',
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:url_launcher/url_launcher.dart';
import '../admin/admin_api.dart';
import '../di/injector.dart';
import '../di/locator.dart';
import '../features/analytics/analytics.dart';
import '../managers/repository/dio_provider.dart';
import '../theme/themes.dart';
import '../widgets/shared_pref_button.dart';
@ -125,7 +128,15 @@ class SettingsPage extends StatelessWidget {
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,
child: GestureDetector(
onTap: () {
locator.purchaseService.buySubscription(plan);
},
child: Stack(
alignment: Alignment.topCenter,

View file

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

View file

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