diff --git a/README.md b/README.md index 12be715..eae9c0b 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,19 @@ keytool -genkeypair -v -keystore kinetra_adehok_app.jks -keyalg RSA -keysize 204 keytool -list -v -keystore kinetra_adehok_app.jks 别名: com.kinetra.adehok.app -创建日期: 2025年8月20日 +创建日期: 2025年9月22日 条目类型: PrivateKeyEntry 证书链长度: 1 证书[1]: -所有者: CN=zy, OU=qj, O=qj, L=cs, ST=hn, C=cn -发布者: CN=zy, OU=qj, O=qj, L=cs, ST=hn, C=cn -序列号: 45fb9110ec85ed35 -生效时间: Wed Aug 20 13:44:03 CST 2025, 失效时间: Sun Jan 05 13:44:03 CST 2053 +所有者: CN=qj, OU=qj, O=qj, L=cs, ST=hunan, C=cn +发布者: CN=qj, OU=qj, O=qj, L=cs, ST=hunan, C=cn +序列号: f98a0bc4e952426a +生效时间: Mon Sep 22 16:02:03 CST 2025, 失效时间: Fri Feb 07 16:02:03 CST 2053 证书指纹: -SHA1: E8:3F:DB:87:73:40:A6:0E:BA:43:C5:C5:42:62:D2:95:FA:E0:DA:21 -SHA256: C5:0A:88:AA:B8:1E:C2:0A:6C:64:0E:99:18:4E:97:86:A0:C8:3F:2C:98:EE:E9:97:6A:A2:CC:B0:03:70:EE:56 +SHA1: 73:61:84:4F:97:9C:EC:B6:5C:25:64:E9:98:51:2C:2E:67:07:1F:AC +SHA256: 73:17:E7:6B:6A:7A:E8:32:72:AE:60:A2:18:78:55:2C:8D:49:66:E3:3C:E9:7E:D6:F3:88:6A:FD:F5:50:33:C1 签名算法名称: SHA384withRSA 主体公共密钥算法: 2048 位 RSA 密钥 -版本: 3 # 监听model修改,自动生成 dart run build_runner watch --delete-conflicting-outputs diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7bfda37..d196f0f 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -4,6 +4,11 @@ plugins { // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } +val signingProperties = + project.rootProject.file("key.properties").takeIf { it.exists() } + ?.reader() + ?.use { Properties().apply { load(it) } } + ?: Properties() android { namespace = "com.kinetra.adehok.app" @@ -29,13 +34,44 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + + ndk { + abiFilters.clear() + abiFilters.add("armeabi-v7a") + abiFilters.add("arm64-v8a") + } } + + signingConfigs { + create("release") { + storeFile = file(signingProperties["storeFile"].toString()) + storePassword = signingProperties["storePassword"].toString() + keyAlias = signingProperties["keyAlias"].toString() + keyPassword = signingProperties["keyPassword"].toString() + } + } + splits { + abi { + isEnable = false + } + } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + + // 签名配置 + signingConfig = signingConfigs.getByName("release") + + } + debug { + signingConfig = signingConfigs.getByName("release") + } } } diff --git a/assets/ic_lock.png b/assets/ic_lock.png new file mode 100644 index 0000000..ca62de4 Binary files /dev/null and b/assets/ic_lock.png differ diff --git a/assets/ic_order_record.png b/assets/ic_order_record.png new file mode 100644 index 0000000..7c0c0b1 Binary files /dev/null and b/assets/ic_order_record.png differ diff --git a/assets/ic_polygon.png b/assets/ic_polygon.png new file mode 100644 index 0000000..53a480b Binary files /dev/null and b/assets/ic_polygon.png differ diff --git a/assets/lock_black.png b/assets/lock_black.png new file mode 100644 index 0000000..93fe055 Binary files /dev/null and b/assets/lock_black.png differ diff --git a/assets/lock_white.png b/assets/lock_white.png new file mode 100644 index 0000000..e289e76 Binary files /dev/null and b/assets/lock_white.png differ diff --git a/assets/recharge_bg.png b/assets/recharge_bg.png new file mode 100644 index 0000000..20861f7 Binary files /dev/null and b/assets/recharge_bg.png differ diff --git a/assets/unlock_bg.png b/assets/unlock_bg.png new file mode 100644 index 0000000..055b979 Binary files /dev/null and b/assets/unlock_bg.png differ diff --git a/lib/dio_cilent/kt_apis.dart b/lib/dio_cilent/kt_apis.dart index 590c164..b984afb 100644 --- a/lib/dio_cilent/kt_apis.dart +++ b/lib/dio_cilent/kt_apis.dart @@ -93,7 +93,7 @@ class KtApis { static String WEB_SITE_FEEDBACK_DETAIL = "${WEB_SITE_HOST}pages/leave/detail"; ///钱包 - static String WEB_SITE_WALLET = "${WEB_SITE_HOST}pages/leave/detail"; + static String WEB_SITE_WALLET = "${WEB_SITE_HOST}pages/order/kinetra"; ///搜索 static String WEB_SITE_SEARCH = "${WEB_SITE_HOST}pages/search/kinetra"; diff --git a/lib/kt_model/kt_goods_bean.dart b/lib/kt_model/kt_goods_bean.dart new file mode 100644 index 0000000..0ea8e2b --- /dev/null +++ b/lib/kt_model/kt_goods_bean.dart @@ -0,0 +1,709 @@ +import 'dart:convert'; + +import 'package:in_app_purchase/in_app_purchase.dart'; + +/// id : 93 +/// status : "enable" +/// price : "59.99" +/// coins : 6000 +/// send_coins : 6000 +/// buy_type : "coins" +/// sort : 12 +/// description : "" +/// vip_type : "" +/// title : "59.99 for coins" +/// brief : "" +/// created_at : "2025-04-15 07:01:47" +/// origin_price : "0.00" +/// backhaul_price : "59.99" +/// ios_template_id : "coins_59.99" +/// android_template_id : "coins_59.99" +/// currency : "US$" +/// updated_at : "2025-07-02 06:48:47" +/// translate_key : "" +/// platform : "all" +/// lang_id : 0 +/// corner_marker : "Popularity" +/// first_promotion_price : "0.00" +/// version : 2 +/// user_level : "high" +/// vip_days : 0 +/// user_promise_level : "" +/// use_once : 0 +/// send_coin_ttl : 1 +/// backhaul_percent : 100 +/// pay_template_id : 2 +/// deleted_at : "" +/// pay_product_id : 14 +/// ext_info : "{\"size\": \"big\", \"backhaul_percent_conf\": []}" +/// factor : "1.00" +/// short_type : "" +/// vip_type_key : "" +/// auto_sub : "Automatic renewal, cancel at any time" +/// size : "big" + +KtGoodsBean ktGoodsBeanFromJson(String str) => KtGoodsBean.fromJson(json.decode(str)); + +String ktGoodsBeanToJson(KtGoodsBean data) => json.encode(data.toJson()); + +class KtGoodsBean { + KtGoodsBean({ + int? id, + String? status, + String? price, + int? coins, + int? sendCoins, + String? buyType, + int? sort, + String? description, + String? vipType, + String? title, + String? brief, + String? createdAt, + String? originPrice, + String? backhaulPrice, + String? iosTemplateId, + String? androidTemplateId, + String? currency, + String? updatedAt, + String? translateKey, + String? platform, + int? langId, + String? cornerMarker, + String? firstPromotionPrice, + int? version, + String? userLevel, + int? vipDays, + String? userPromiseLevel, + int? useOnce, + int? sendCoinTtl, + int? backhaulPercent, + int? payTemplateId, + String? deletedAt, + int? payProductId, + ExtInfo? extInfo, + String? factor, + String? shortType, + String? vipTypeKey, + String? autoSub, + String? size, + String? orderCode, + String? transactionId, + String? serverVerificationData, + ProductDetails? productDetails, + }) { + _id = id; + _status = status; + _price = price; + _coins = coins; + _sendCoins = sendCoins; + _buyType = buyType; + _sort = sort; + _description = description; + _vipType = vipType; + _title = title; + _brief = brief; + _createdAt = createdAt; + _originPrice = originPrice; + _backhaulPrice = backhaulPrice; + _iosTemplateId = iosTemplateId; + _androidTemplateId = androidTemplateId; + _currency = currency; + _updatedAt = updatedAt; + _translateKey = translateKey; + _platform = platform; + _langId = langId; + _cornerMarker = cornerMarker; + _firstPromotionPrice = firstPromotionPrice; + _version = version; + _userLevel = userLevel; + _vipDays = vipDays; + _userPromiseLevel = userPromiseLevel; + _useOnce = useOnce; + _sendCoinTtl = sendCoinTtl; + _backhaulPercent = backhaulPercent; + _payTemplateId = payTemplateId; + _deletedAt = deletedAt; + _payProductId = payProductId; + _extInfo = extInfo; + _factor = factor; + _shortType = shortType; + _vipTypeKey = vipTypeKey; + _autoSub = autoSub; + _size = size; + _orderCode = orderCode; + _transactionId = transactionId; + _serverVerificationData = serverVerificationData; + _productDetails = productDetails; + } + + KtGoodsBean.fromJson(dynamic json) { + _id = json['id']; + _status = json['status']; + _price = json['price']; + _coins = json['coins']; + _sendCoins = json['send_coins']; + _buyType = json['buy_type']; + _sort = json['sort']; + _description = json['description']; + _vipType = json['vip_type']; + _title = json['title']; + _brief = json['brief']; + _createdAt = json['created_at']; + _originPrice = json['origin_price']; + _backhaulPrice = json['backhaul_price']; + _iosTemplateId = json['ios_template_id']; + _androidTemplateId = json['android_template_id']; + _currency = json['currency']; + _updatedAt = json['updated_at']; + _translateKey = json['translate_key']; + _platform = json['platform']; + _langId = json['lang_id']; + _cornerMarker = json['corner_marker']; + _firstPromotionPrice = json['first_promotion_price']; + _version = json['version']; + _userLevel = json['user_level']; + _vipDays = json['vip_days']; + _userPromiseLevel = json['user_promise_level']; + _useOnce = json['use_once']; + _sendCoinTtl = json['send_coin_ttl']; + _backhaulPercent = json['backhaul_percent']; + _payTemplateId = json['pay_template_id']; + _deletedAt = json['deleted_at']; + _payProductId = json['pay_product_id']; + _extInfo = json['ext_info'] != null ? ExtInfo.fromJson(json['ext_info']) : null; + _factor = json['factor']; + _shortType = json['short_type']; + _vipTypeKey = json['vip_type_key']; + _autoSub = json['auto_sub']; + _size = json['size']; + _orderCode = json['orderCode']; + _transactionId = json['transactionId']; + _serverVerificationData = json['serverVerificationData']; + _productDetails = json['productDetails']; + } + + int? _id; + String? _status; + String? _price; + int? _coins; + int? _sendCoins; + String? _buyType; + int? _sort; + String? _description; + String? _vipType; + String? _title; + String? _brief; + String? _createdAt; + String? _originPrice; + String? _backhaulPrice; + String? _iosTemplateId; + String? _androidTemplateId; + String? _currency; + String? _updatedAt; + String? _translateKey; + String? _platform; + int? _langId; + String? _cornerMarker; + String? _firstPromotionPrice; + int? _version; + String? _userLevel; + int? _vipDays; + String? _userPromiseLevel; + int? _useOnce; + int? _sendCoinTtl; + int? _backhaulPercent; + int? _payTemplateId; + String? _deletedAt; + int? _payProductId; + ExtInfo? _extInfo; + String? _factor; + String? _shortType; + String? _vipTypeKey; + String? _autoSub; + String? _size; + String? _orderCode; + String? _transactionId; + String? _serverVerificationData; + ProductDetails? _productDetails; + + KtGoodsBean copyWith({ + int? id, + String? status, + String? price, + int? coins, + int? sendCoins, + String? buyType, + int? sort, + String? description, + String? vipType, + String? title, + String? brief, + String? createdAt, + String? originPrice, + String? backhaulPrice, + String? iosTemplateId, + String? androidTemplateId, + String? currency, + String? updatedAt, + String? translateKey, + String? platform, + int? langId, + String? cornerMarker, + String? firstPromotionPrice, + int? version, + String? userLevel, + int? vipDays, + String? userPromiseLevel, + int? useOnce, + int? sendCoinTtl, + int? backhaulPercent, + int? payTemplateId, + String? deletedAt, + int? payProductId, + ExtInfo? extInfo, + String? factor, + String? shortType, + String? vipTypeKey, + String? autoSub, + String? size, + String? orderCode, + String? transactionId, + String? serverVerificationData, + ProductDetails? productDetails, + }) => KtGoodsBean( + id: id ?? _id, + status: status ?? _status, + price: price ?? _price, + coins: coins ?? _coins, + sendCoins: sendCoins ?? _sendCoins, + buyType: buyType ?? _buyType, + sort: sort ?? _sort, + description: description ?? _description, + vipType: vipType ?? _vipType, + title: title ?? _title, + brief: brief ?? _brief, + createdAt: createdAt ?? _createdAt, + originPrice: originPrice ?? _originPrice, + backhaulPrice: backhaulPrice ?? _backhaulPrice, + iosTemplateId: iosTemplateId ?? _iosTemplateId, + androidTemplateId: androidTemplateId ?? _androidTemplateId, + currency: currency ?? _currency, + updatedAt: updatedAt ?? _updatedAt, + translateKey: translateKey ?? _translateKey, + platform: platform ?? _platform, + langId: langId ?? _langId, + cornerMarker: cornerMarker ?? _cornerMarker, + firstPromotionPrice: firstPromotionPrice ?? _firstPromotionPrice, + version: version ?? _version, + userLevel: userLevel ?? _userLevel, + vipDays: vipDays ?? _vipDays, + userPromiseLevel: userPromiseLevel ?? _userPromiseLevel, + useOnce: useOnce ?? _useOnce, + sendCoinTtl: sendCoinTtl ?? _sendCoinTtl, + backhaulPercent: backhaulPercent ?? _backhaulPercent, + payTemplateId: payTemplateId ?? _payTemplateId, + deletedAt: deletedAt ?? _deletedAt, + payProductId: payProductId ?? _payProductId, + extInfo: extInfo ?? _extInfo, + factor: factor ?? _factor, + shortType: shortType ?? _shortType, + vipTypeKey: vipTypeKey ?? _vipTypeKey, + autoSub: autoSub ?? _autoSub, + size: size ?? _size, + orderCode: orderCode ?? _orderCode, + transactionId: transactionId ?? _transactionId, + serverVerificationData: serverVerificationData ?? _serverVerificationData, + productDetails: productDetails ?? _productDetails, + ); + + int? get id => _id; + + String? get status => _status; + + String? get price => _price; + + int? get coins => _coins; + + int? get sendCoins => _sendCoins; + + String? get buyType => _buyType; + + int? get sort => _sort; + + String? get description => _description; + + String? get vipType => _vipType; + + String? get title => _title; + + String? get brief => _brief; + + String? get createdAt => _createdAt; + + String? get originPrice => _originPrice; + + String? get backhaulPrice => _backhaulPrice; + + String? get iosTemplateId => _iosTemplateId; + + String? get androidTemplateId => _androidTemplateId; + + String? get currency => _currency; + + String? get updatedAt => _updatedAt; + + String? get translateKey => _translateKey; + + String? get platform => _platform; + + int? get langId => _langId; + + String? get cornerMarker => _cornerMarker; + + String? get firstPromotionPrice => _firstPromotionPrice; + + int? get version => _version; + + String? get userLevel => _userLevel; + + int? get vipDays => _vipDays; + + String? get userPromiseLevel => _userPromiseLevel; + + int? get useOnce => _useOnce; + + int? get sendCoinTtl => _sendCoinTtl; + + int? get backhaulPercent => _backhaulPercent; + + int? get payTemplateId => _payTemplateId; + + String? get deletedAt => _deletedAt; + + int? get payProductId => _payProductId; + + ExtInfo? get extInfo => _extInfo; + + String? get factor => _factor; + + String? get shortType => _shortType; + + String? get vipTypeKey => _vipTypeKey; + + String? get autoSub => _autoSub; + + String? get size => _size; + + String? get orderCode => _orderCode; + + String? get transactionId => _transactionId; + + String? get serverVerificationData => _serverVerificationData; + + ProductDetails? get productDetails => _productDetails; + + // set方法补充 + set id(int? value) => _id = value; + + set status(String? value) => _status = value; + + set price(String? value) => _price = value; + + set coins(int? value) => _coins = value; + + set sendCoins(int? value) => _sendCoins = value; + + set buyType(String? value) => _buyType = value; + + set sort(int? value) => _sort = value; + + set description(String? value) => _description = value; + + set vipType(String? value) => _vipType = value; + + set title(String? value) => _title = value; + + set brief(String? value) => _brief = value; + + set createdAt(String? value) => _createdAt = value; + + set originPrice(String? value) => _originPrice = value; + + set backhaulPrice(String? value) => _backhaulPrice = value; + + set iosTemplateId(String? value) => _iosTemplateId = value; + + set androidTemplateId(String? value) => _androidTemplateId = value; + + set currency(String? value) => _currency = value; + + set updatedAt(String? value) => _updatedAt = value; + + set translateKey(String? value) => _translateKey = value; + + set platform(String? value) => _platform = value; + + set langId(int? value) => _langId = value; + + set cornerMarker(String? value) => _cornerMarker = value; + + set firstPromotionPrice(String? value) => _firstPromotionPrice = value; + + set version(int? value) => _version = value; + + set userLevel(String? value) => _userLevel = value; + + set vipDays(int? value) => _vipDays = value; + + set userPromiseLevel(String? value) => _userPromiseLevel = value; + + set useOnce(int? value) => _useOnce = value; + + set sendCoinTtl(int? value) => _sendCoinTtl = value; + + set backhaulPercent(int? value) => _backhaulPercent = value; + + set payTemplateId(int? value) => _payTemplateId = value; + + set deletedAt(String? value) => _deletedAt = value; + + set payProductId(int? value) => _payProductId = value; + + set extInfo(ExtInfo? value) => _extInfo = value; + + set factor(String? value) => _factor = value; + + set shortType(String? value) => _shortType = value; + + set vipTypeKey(String? value) => _vipTypeKey = value; + + set autoSub(String? value) => _autoSub = value; + + set size(String? value) => _size = value; + + set orderCode(String? value) => _orderCode = value; + + set transactionId(String? value) => _transactionId = value; + + set serverVerificationData(String? value) => _serverVerificationData = value; + + set productDetails(ProductDetails? value) => _productDetails = value; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['status'] = _status; + map['price'] = _price; + map['coins'] = _coins; + map['send_coins'] = _sendCoins; + map['buy_type'] = _buyType; + map['sort'] = _sort; + map['description'] = _description; + map['vip_type'] = _vipType; + map['title'] = _title; + map['brief'] = _brief; + map['created_at'] = _createdAt; + map['origin_price'] = _originPrice; + map['backhaul_price'] = _backhaulPrice; + map['ios_template_id'] = _iosTemplateId; + map['android_template_id'] = _androidTemplateId; + map['currency'] = _currency; + map['updated_at'] = _updatedAt; + map['translate_key'] = _translateKey; + map['platform'] = _platform; + map['lang_id'] = _langId; + map['corner_marker'] = _cornerMarker; + map['first_promotion_price'] = _firstPromotionPrice; + map['version'] = _version; + map['user_level'] = _userLevel; + map['vip_days'] = _vipDays; + map['user_promise_level'] = _userPromiseLevel; + map['use_once'] = _useOnce; + map['send_coin_ttl'] = _sendCoinTtl; + map['backhaul_percent'] = _backhaulPercent; + map['pay_template_id'] = _payTemplateId; + map['deleted_at'] = _deletedAt; + map['pay_product_id'] = _payProductId; + if (_extInfo != null) { + map['ext_info'] = _extInfo?.toJson(); + } + map['factor'] = _factor; + map['short_type'] = _shortType; + map['vip_type_key'] = _vipTypeKey; + map['auto_sub'] = _autoSub; + map['size'] = _size; + map['serverVerificationData'] = _serverVerificationData; + map['transactionId'] = _transactionId; + map['orderCode'] = _orderCode; + // map['productDetails'] = _productDetails; + return map; + } +} + +/// size : "small" +/// extra_coins : 0 +/// max_total_coins : 0 +/// max_total_coins_pop : 0 +/// is_vip_show : false +/// receive_coins_rate : 85 +/// extra_day_coins : 85 +/// sub_coins_txt_list : ["",""] +/// backhaul_percent_conf : [{"episode_num":50,"backhaul_percent":50},{"episode_num":80,"backhaul_percent":80}] + +ExtInfo extInfoFromJson(String str) => ExtInfo.fromJson(json.decode(str)); + +String extInfoToJson(ExtInfo data) => json.encode(data.toJson()); + +class ExtInfo { + ExtInfo({ + String? size, + int? maxTotalCoins, + int? maxTotalCoinsPop, + int? extraCoins, + bool? isVipShow = false, + String? receiveCoinsRate, + int? extraDayCoins, + bool? coinsWinBackShow, + List? subCoinsTxtList, + List? backhaulPercentConf, + }) { + _size = size; + _maxTotalCoins = maxTotalCoins; + _maxTotalCoinsPop = maxTotalCoinsPop; + _extraCoins = extraCoins; + _isVipShow = isVipShow; + _receiveCoinsRate = receiveCoinsRate; + _extraDayCoins = extraDayCoins; + _coinsWinBackShow = coinsWinBackShow; + _subCoinsTxtList = subCoinsTxtList; + _backhaulPercentConf = backhaulPercentConf; + } + + ExtInfo.fromJson(dynamic json) { + _size = json['size']; + _maxTotalCoins = json['max_total_coins']; + _maxTotalCoinsPop = json['max_total_coins_pop']; + _isVipShow = json['is_vip_show']; + _receiveCoinsRate = json['receive_coins_rate']; + _extraDayCoins = json['extra_day_coins']; + _extraCoins = json['extra_coins']; + _coinsWinBackShow = json['coins_win_back_show']; + _subCoinsTxtList = json['sub_coins_txt_list'] != null ? json['sub_coins_txt_list'].cast() : []; + if (json['backhaul_percent_conf'] != null) { + _backhaulPercentConf = []; + json['backhaul_percent_conf'].forEach((v) { + _backhaulPercentConf?.add(BackhaulPercentConf.fromJson(v)); + }); + } + } + + String? _size; + int? _extraCoins; + int? _maxTotalCoins; + int? _maxTotalCoinsPop; + String? _receiveCoinsRate; + int? _extraDayCoins; + bool? _isVipShow; + bool? _coinsWinBackShow; + List? _subCoinsTxtList; + List? _backhaulPercentConf; + + ExtInfo copyWith({ + String? size, + int? extraCoins, + int? maxTotalCoins, + int? maxTotalCoinsPop, + String? receiveCoinsRate, + int? extraDayCoins, + bool? coinsWinBackShow, + bool? isVipShow, + List? subCoinsTxtList, + List? backhaulPercentConf, + }) => ExtInfo( + size: size ?? _size, + maxTotalCoins: maxTotalCoins ?? _maxTotalCoins, + maxTotalCoinsPop: maxTotalCoinsPop ?? _maxTotalCoinsPop, + receiveCoinsRate: receiveCoinsRate ?? _receiveCoinsRate, + extraDayCoins: extraDayCoins ?? _extraDayCoins, + isVipShow: isVipShow ?? _isVipShow, + extraCoins: extraCoins ?? _extraCoins, + coinsWinBackShow: coinsWinBackShow ?? _coinsWinBackShow, + subCoinsTxtList: subCoinsTxtList ?? _subCoinsTxtList, + backhaulPercentConf: backhaulPercentConf ?? _backhaulPercentConf, + ); + + String? get size => _size; + + int? get extraCoins => _extraCoins; + + int? get maxTotalCoins => _maxTotalCoins; + + int? get maxTotalCoinsPop => _maxTotalCoinsPop; + + String? get receiveCoinsRate => _receiveCoinsRate; + + int? get extraDayCoins => _extraDayCoins; + + bool? get coinsWinBackShow => _coinsWinBackShow; + + bool? get isVipShow => _isVipShow; + + List? get subCoinsTxtList => _subCoinsTxtList; + + List? get backhaulPercentConf => _backhaulPercentConf; + + Map toJson() { + final map = {}; + map['size'] = _size; + map['max_total_coins'] = _maxTotalCoins; + map['max_total_coins_pop'] = _maxTotalCoinsPop; + map['receive_coins_rate'] = _receiveCoinsRate; + map['extra_day_coins'] = _extraDayCoins; + map['is_vip_show'] = _isVipShow; + map['extra_coins'] = _extraCoins; + map['coins_win_back_show'] = _coinsWinBackShow; + map['sub_coins_txt_list'] = _subCoinsTxtList; + if (_backhaulPercentConf != null) { + map['backhaul_percent_conf'] = _backhaulPercentConf?.map((v) => v.toJson()).toList(); + } + return map; + } +} + +/// episode_num : 50 +/// backhaul_percent : 50 + +BackhaulPercentConf backhaulPercentConfFromJson(String str) => BackhaulPercentConf.fromJson(json.decode(str)); + +String backhaulPercentConfToJson(BackhaulPercentConf data) => json.encode(data.toJson()); + +class BackhaulPercentConf { + BackhaulPercentConf({int? episodeNum, int? backhaulPercent}) { + _episodeNum = episodeNum; + _backhaulPercent = backhaulPercent; + } + + BackhaulPercentConf.fromJson(dynamic json) { + _episodeNum = json['episode_num']; + _backhaulPercent = json['backhaul_percent']; + } + + int? _episodeNum; + int? _backhaulPercent; + + BackhaulPercentConf copyWith({int? episodeNum, int? backhaulPercent}) => + BackhaulPercentConf(episodeNum: episodeNum ?? _episodeNum, backhaulPercent: backhaulPercent ?? _backhaulPercent); + + int? get episodeNum => _episodeNum; + + int? get backhaulPercent => _backhaulPercent; + + Map toJson() { + final map = {}; + map['episode_num'] = _episodeNum; + map['backhaul_percent'] = _backhaulPercent; + return map; + } +} + diff --git a/lib/kt_model/kt_order_bean.dart b/lib/kt_model/kt_order_bean.dart new file mode 100644 index 0000000..a9ead87 --- /dev/null +++ b/lib/kt_model/kt_order_bean.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +/// order_code : "O202507111000016870c46c4c3fd" +/// money : "9.99" +/// is_backhaul : 2 + +KtOrderBean ktOrderBeanFromJson(String str) => KtOrderBean.fromJson(json.decode(str)); +String ktOrderBeanToJson(KtOrderBean data) => json.encode(data.toJson()); + +class KtOrderBean { + KtOrderBean({String? orderCode, String? money, int? isBackhaul}) { + _orderCode = orderCode; + _money = money; + _isBackhaul = isBackhaul; + } + + KtOrderBean.fromJson(dynamic json) { + _orderCode = json['order_code']; + _money = json['money']; + _isBackhaul = json['is_backhaul']; + } + String? _orderCode; + String? _money; + int? _isBackhaul; + KtOrderBean copyWith({String? orderCode, String? money, int? isBackhaul}) => + KtOrderBean(orderCode: orderCode ?? _orderCode, money: money ?? _money, isBackhaul: isBackhaul ?? _isBackhaul); + String? get orderCode => _orderCode; + String? get money => _money; + int? get isBackhaul => _isBackhaul; + + Map toJson() { + final map = {}; + map['order_code'] = _orderCode; + map['money'] = _money; + map['is_backhaul'] = _isBackhaul; + return map; + } +} diff --git a/lib/kt_model/kt_store_bean.dart b/lib/kt_model/kt_store_bean.dart new file mode 100644 index 0000000..977661f --- /dev/null +++ b/lib/kt_model/kt_store_bean.dart @@ -0,0 +1,159 @@ +import 'dart:convert'; + +import 'kt_goods_bean.dart'; + +/// sort : ["list_coins","list_sub_vip"] +/// list_vip : [{"id":93,"status":"enable","price":"59.99","coins":6000,"send_coins":6000,"buy_type":"coins","sort":12,"description":"","vip_type":"","title":"59.99 for coins","brief":"","created_at":"2025-04-15 07:01:47","origin_price":"0.00","backhaul_price":"59.99","ios_template_id":"coins_59.99","android_template_id":"coins_59.99","currency":"US$","updated_at":"2025-07-02 06:48:47","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"","use_once":0,"send_coin_ttl":1,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"","pay_product_id":14,"ext_info":"{\"size\": \"big\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"","vip_type_key":"","auto_sub":"Automatic renewal, cancel at any time","size":"big"}] +/// list_coins : [{"id":93,"status":"enable","price":"59.99","coins":6000,"send_coins":6000,"buy_type":"coins","sort":12,"description":"","vip_type":"","title":"59.99 for coins","brief":"","created_at":"2025-04-15 07:01:47","origin_price":"0.00","backhaul_price":"59.99","ios_template_id":"coins_59.99","android_template_id":"coins_59.99","currency":"US$","updated_at":"2025-07-02 06:48:47","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"","use_once":0,"send_coin_ttl":1,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"","pay_product_id":14,"ext_info":"{\"size\": \"big\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"","vip_type_key":"","auto_sub":"Automatic renewal, cancel at any time","size":"big"}] +/// list_sub_vip : [{"id":104,"status":"enable","price":"49.99","coins":0,"send_coins":4500,"buy_type":"sub_vip","sort":5,"description":"Unlimited access to all series for 1 month (Ad-free)","vip_type":"month","title":"Monthly membership","brief":"Monthly membership","created_at":"2025-04-15 07:30:47","origin_price":"0.00","backhaul_price":"24.99","ios_template_id":"sub.m_49.99","android_template_id":"sub.m_49.99","currency":"US$","updated_at":"2025-06-20 07:27:23","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"null","use_once":0,"send_coin_ttl":35,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"null","pay_product_id":25,"ext_info":"{\"size\": \"\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"Monthly","vip_type_key":"month","auto_sub":"Automatic renewal, cancel at any time","size":"small"}] +/// list_retrieve : [{"id":93,"status":"enable","price":"59.99","coins":6000,"send_coins":6000,"buy_type":"coins","sort":12,"description":"","vip_type":"","title":"59.99 for coins","brief":"","created_at":"2025-04-15 07:01:47","origin_price":"0.00","backhaul_price":"59.99","ios_template_id":"coins_59.99","android_template_id":"coins_59.99","currency":"US$","updated_at":"2025-07-02 06:48:47","translate_key":"","platform":"all","lang_id":0,"corner_marker":"Popularity","first_promotion_price":"0.00","version":2,"user_level":"high","vip_days":0,"user_promise_level":"","use_once":0,"send_coin_ttl":1,"backhaul_percent":100,"pay_template_id":2,"deleted_at":"","pay_product_id":14,"ext_info":"{\"size\": \"big\", \"backhaul_percent_conf\": []}","factor":"1.00","short_type":"","vip_type_key":"","auto_sub":"Automatic renewal, cancel at any time","size":"big"}] + +KtStoreBean ktStoreBeanFromJson(String str) => KtStoreBean.fromJson(json.decode(str)); + +String ktStoreBeanToJson(KtStoreBean data) => json.encode(data.toJson()); + +class KtStoreBean { + KtStoreBean({ + int? showType, + int? payMode, + List? sort, + List? listVip, + List? listCoins, + List? listSubCoins, + List? listSubVip, + List? listRetrieve, + }) { + _sort = sort; + _payMode = payMode; + _showType = showType; + _listVip = listVip; + _listCoins = listCoins; + _listSubCoins = listSubCoins; + _listSubVip = listSubVip; + _listRetrieve = listRetrieve; + } + + KtStoreBean.fromJson(dynamic json) { + _showType = json['show_type']; + _payMode = json['pay_mode']; + _sort = json['sort'] != null ? json['sort'].cast() : []; + if (json['list_vip'] != null) { + _listVip = []; + json['list_vip'].forEach((v) { + _listVip?.add(KtGoodsBean.fromJson(v)); + }); + } + if (json['list_coins'] != null) { + _listCoins = []; + json['list_coins'].forEach((v) { + _listCoins?.add(KtGoodsBean.fromJson(v)); + }); + } + if (json['list_sub_coins'] != null) { + _listSubCoins = []; + json['list_sub_coins'].forEach((v) { + _listSubCoins?.add(KtGoodsBean.fromJson(v)); + }); + } + if (json['list_sub_vip'] != null) { + _listSubVip = []; + json['list_sub_vip'].forEach((v) { + _listSubVip?.add(KtGoodsBean.fromJson(v)); + }); + } + if (json['list_retrieve'] != null) { + _listRetrieve = []; + json['list_retrieve'].forEach((v) { + _listRetrieve?.add(KtGoodsBean.fromJson(v)); + }); + } + } + + int? _payMode; + int? _showType; + List? _sort; + List? _listVip; + List? _listCoins; + List? _listSubCoins; + List? _listSubVip; + List? _listRetrieve; + + KtStoreBean copyWith({ + int? payMode, + int? showType, + List? sort, + List? listVip, + List? listCoins, + List? listSubCoins, + List? listSubVip, + List? listRetrieve, + }) => KtStoreBean( + payMode: payMode ?? _payMode, + showType: showType ?? _showType, + sort: sort ?? _sort, + listVip: listVip ?? _listVip, + listCoins: listCoins ?? _listCoins, + listSubVip: listSubVip ?? _listSubVip, + listSubCoins: listSubCoins ?? _listSubCoins, + listRetrieve: listRetrieve ?? _listRetrieve, + ); + + int? get payMode => _payMode; + + int? get showType => _showType; + + List? get sort => _sort; + + List? get listVip => _listVip; + + List? get listCoins => _listCoins; + + List? get listSubCoins => _listSubCoins; + + List? get listSubVip => _listSubVip; + + List? get listRetrieve => _listRetrieve; + + Map toJson() { + final map = {}; + map['show_type'] = _showType; + map['pay_mode'] = _payMode; + map['sort'] = _sort; + if (_listVip != null) { + map['list_vip'] = _listVip?.map((v) => v.toJson()).toList(); + } + if (_listCoins != null) { + map['list_coins'] = _listCoins?.map((v) => v.toJson()).toList(); + } + if (_listSubVip != null) { + map['list_sub_vip'] = _listSubVip?.map((v) => v.toJson()).toList(); + } + if (_listSubCoins != null) { + map['list_sub_coins'] = _listSubCoins?.map((v) => v.toJson()).toList(); + } + if (_listRetrieve != null) { + map['list_retrieve'] = _listRetrieve?.map((v) => v.toJson()).toList(); + } + return map; + } +} + +KtGoodsBean listRetrieveFromJson(String str) => KtGoodsBean.fromJson(json.decode(str)); + +String listRetrieveToJson(KtGoodsBean data) => json.encode(data.toJson()); + +KtGoodsBean listSubVipFromJson(String str) => KtGoodsBean.fromJson(json.decode(str)); + +String listSubVipToJson(KtGoodsBean data) => json.encode(data.toJson()); + +KtGoodsBean listSubCoinsFromJson(String str) => KtGoodsBean.fromJson(json.decode(str)); + +String listSubCoinsToJson(KtGoodsBean data) => json.encode(data.toJson()); + +KtGoodsBean listCoinsFromJson(String str) => KtGoodsBean.fromJson(json.decode(str)); + +String listCoinsToJson(KtGoodsBean data) => json.encode(data.toJson()); + +KtGoodsBean listVipFromJson(String str) => KtGoodsBean.fromJson(json.decode(str)); + +String listVipToJson(KtGoodsBean data) => json.encode(data.toJson()); diff --git a/lib/kt_pages/kt_home/kt_search_page.dart b/lib/kt_pages/kt_home/kt_search_page.dart index 0749972..8ac6d09 100644 --- a/lib/kt_pages/kt_home/kt_search_page.dart +++ b/lib/kt_pages/kt_home/kt_search_page.dart @@ -154,7 +154,7 @@ class SignInActivityPageState extends State with RouteAware { allowedOriginRules: {"*"}, onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) { - print('----callback--jsonS:$message'); + debugPrint('----callback--jsonS:$message'); if (message?.data != null) { _handleWebMessage(message?.data); } @@ -247,7 +247,7 @@ class SignInActivityPageState extends State with RouteAware { } Widget _buildWidget() { - print('----loadStatus:$loadingStatus'); + debugPrint('----loadStatus:$loadingStatus'); switch (loadingStatus) { case KtLoadStatusType.loading: return Center(child: CircularProgressIndicator()); diff --git a/lib/kt_pages/kt_main_page/view.dart b/lib/kt_pages/kt_main_page/view.dart index 4710dc7..273819c 100644 --- a/lib/kt_pages/kt_main_page/view.dart +++ b/lib/kt_pages/kt_main_page/view.dart @@ -4,7 +4,6 @@ import 'package:flutter_kinetra/kt_pages/kt_actor/kt_actor_page.dart'; import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../kt_utils/kt_keys.dart'; import '../kt_explore/view.dart'; import '../kt_home/view.dart'; import '../kt_mine/view.dart'; @@ -23,7 +22,7 @@ class _KtMainPageState extends State static const List _tabsTitle = [ {'icon': 'home', 'title': 'Home'}, {'icon': 'explore', 'title': 'Explore'}, - {'icon': 'actor', 'title': 'Actor'}, + {'icon': 'actor', 'title': 'Actors'}, {'icon': 'favorite', 'title': 'My List'}, {'icon': 'mine', 'title': 'Profile'}, ]; diff --git a/lib/kt_pages/kt_mine/insert_web/wallet_page.dart b/lib/kt_pages/kt_mine/insert_web/kt_order_record_page.dart similarity index 60% rename from lib/kt_pages/kt_mine/insert_web/wallet_page.dart rename to lib/kt_pages/kt_mine/insert_web/kt_order_record_page.dart index c897cd1..6c24e27 100644 --- a/lib/kt_pages/kt_mine/insert_web/wallet_page.dart +++ b/lib/kt_pages/kt_mine/insert_web/kt_order_record_page.dart @@ -12,20 +12,21 @@ import 'package:get/get.dart'; import '../../../dio_cilent/kt_apis.dart'; import '../../../kt_utils/kt_keys.dart'; import '../../../kt_utils/kt_utils.dart'; +import '../../../kt_widgets/kt_status_widget.dart'; import '../../../main.dart'; -class WalletPage extends StatefulWidget { - const WalletPage({super.key}); +class KtOrderRecordPage extends StatefulWidget { + const KtOrderRecordPage({super.key}); @override - SignInActivityPageState createState() => SignInActivityPageState(); + KtOrderRecordPageState createState() => KtOrderRecordPageState(); } -class SignInActivityPageState extends State with RouteAware { +class KtOrderRecordPageState extends State with RouteAware { InAppWebViewController? _webViewController; late PullToRefreshController _webRefreshController; late Map _userData; - // LoadStatusType loadingStatus = LoadStatusType.loading; + KtLoadStatusType loadingStatus = KtLoadStatusType.loading; @override void initState() { @@ -39,10 +40,10 @@ class SignInActivityPageState extends State with RouteAware { 'time_zone': KtUtils.getTimeZoneOffset(DateTime.now()), 'type': Platform.isAndroid ? 'android' : 'ios', 'lang': 'en', - 'theme': 'theme_7', + // 'theme': 'theme_7', 'token': SpUtil.getString(KtKeys.token) ?? '', }; - print('-----userData:${_userData}'); + debugPrint('-----userData:$_userData'); } _initRefreshController() { @@ -108,32 +109,24 @@ class SignInActivityPageState extends State with RouteAware { @override Widget build(BuildContext context) { - return Scaffold( - extendBodyBehindAppBar: true, - // appBar: AppBar( - // leading: Container( - // padding: EdgeInsets.only(left: 15.w, bottom: 6.w), - // child: IconButton( - // icon: Image.asset('ic_back.png'.ktIcon, width: 24.w), - // onPressed: () => Navigator.of(context).maybePop(), - // ), - // ), - // ), - body: Container( - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight + 20.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('bg1.png'.ktIcon), - fit: BoxFit.fill, - ), + return Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight + 20.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('bg1.png'.ktIcon), + fit: BoxFit.fill, ), - child: Container( - margin: EdgeInsets.only(top: 16.w), - child: Stack( - children: [ - InAppWebView( + ), + child: Container( + margin: EdgeInsets.only(top: 16.w), + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + margin: EdgeInsets.only(top: 20.w), + child: InAppWebView( pullToRefreshController: _webRefreshController, initialSettings: InAppWebViewSettings( cacheEnabled: false, @@ -156,7 +149,7 @@ class SignInActivityPageState extends State with RouteAware { return window.flutter_inappwebview.callHandler('js2app',jsonS); } }; - + """, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, ), @@ -167,37 +160,26 @@ class SignInActivityPageState extends State with RouteAware { handlerName: 'getUserInfo', callback: (_) => jsonEncode(_userData), ); - if (Platform.isIOS) { - // ios - await _webViewController?.addWebMessageListener( - WebMessageListener( - jsObjectName: "js2app", - allowedOriginRules: {"*"}, - onPostMessage: - (message, sourceOrigin, isMainFrame, replyProxy) { - if (message?.data != null) { - _handleWebMessage(message?.data); - } - }, - ), - ); - } else if (Platform.isAndroid) { - _webViewController?.addJavaScriptHandler( - handlerName: "js2app", - callback: (jsonS) { - if (jsonS.isNotEmpty) { - _handleWebMessage(jsonS.toString()); - } - }, - ); - } + await _webViewController?.addWebMessageListener( + WebMessageListener( + jsObjectName: "goStore", + allowedOriginRules: {"*"}, + onPostMessage: + (message, sourceOrigin, isMainFrame, replyProxy) { + debugPrint('----callback--jsonS:$message'); + if (message != null) { + Get.toNamed(KtRoutes.store); + } + }, + ), + ); await _webViewController?.loadUrl( urlRequest: URLRequest(url: WebUri(KtApis.WEB_SITE_WALLET)), ); }, onLoadStart: (controller, url) { setState(() { - // loadingStatus = LoadStatusType.loading; + loadingStatus = KtLoadStatusType.loading; }); }, onLoadStop: (controller, url) async { @@ -213,25 +195,25 @@ class SignInActivityPageState extends State with RouteAware { controller.evaluateJavascript( source: ''' - if(typeof window.receiveDataFromNative === 'function') { - window.receiveDataFromNative($userJsonStr); - } - ''', + if(typeof window.receiveDataFromNative === 'function') { + window.receiveDataFromNative($userJsonStr); + } + ''', ); }); } else if (Platform.isAndroid) { await controller.evaluateJavascript( source: """ - window.AndroidInterface = { - getUserInfo: async function () { - return window.flutter_inappwebview.callHandler('getUserInfo'); - }, - }; - """, + window.AndroidInterface = { + getUserInfo: async function () { + return window.flutter_inappwebview.callHandler('getUserInfo'); + }, + }; + """, ); } setState(() { - // loadingStatus = LoadStatusType.loadSuccess; + loadingStatus = KtLoadStatusType.loadSuccess; }); _webRefreshController.endRefreshing(); }, @@ -239,16 +221,68 @@ class SignInActivityPageState extends State with RouteAware { _webRefreshController.endRefreshing(); Future.delayed(const Duration(milliseconds: 100)).then((_) { setState(() { - // loadingStatus = LoadStatusType.loadFailed; + loadingStatus = KtLoadStatusType.loadFailed; }); }); }, ), - // _buildWidget(), - ], - ), + ), + _buildWidget(), + Positioned(left: 0,right: 0, child: _buildAppBar()), + ], ), ), ); } + + Widget _buildAppBar() { + return SizedBox( + width: ScreenUtil().screenWidth, + height: 40.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Image.asset('ic_back.png'.ktIcon, width: 8.w), + onPressed: () => Navigator.of(context).maybePop(), + ), + Text( + 'My Wallet', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + Container(width: 24.w), + ], + ), + ); + } + + Widget _buildWidget() { + debugPrint('----loadStatus:$loadingStatus'); + switch (loadingStatus) { + case KtLoadStatusType.loading: + return Center(child: CircularProgressIndicator()); + case KtLoadStatusType.loadFailed: + return KtStatusWidget( + type: KtErrorStatusType.noNetwork, + onPressed: () { + _webViewController?.loadUrl( + urlRequest: URLRequest(url: WebUri(KtApis.WEB_SITE_SEARCH)), + ); + }, + ); + case KtLoadStatusType.loadNoData: + return KtStatusWidget( + type: KtErrorStatusType.notFound, + onPressed: () { + _webViewController?.reload(); + }, + ); + default: + return const SizedBox.shrink(); + } + } } diff --git a/lib/kt_pages/kt_mine/kt_store/logic.dart b/lib/kt_pages/kt_mine/kt_store/logic.dart new file mode 100644 index 0000000..c23e39a --- /dev/null +++ b/lib/kt_pages/kt_mine/kt_store/logic.dart @@ -0,0 +1,529 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_kinetra/kt_pages/kt_mine/kt_store/state.dart'; +import 'package:get/get.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import '../../../dio_cilent/kt_apis.dart'; +import '../../../dio_cilent/kt_request.dart'; +import '../../../kt_model/kt_goods_bean.dart'; +import '../../../kt_model/kt_order_bean.dart'; +import '../../../kt_model/kt_store_bean.dart'; +import '../../../kt_utils/kt_device_info_utils.dart'; +import '../../../kt_utils/kt_iap_util.dart'; +import '../../../kt_utils/kt_purchase_restore_utils.dart'; +import '../../../kt_utils/kt_toast_utils.dart'; +import '../../../kt_utils/kt_utils.dart'; +import '../logic.dart'; + +class KtStoreLogic extends GetxController { + final state = KtStoreState(); + late StreamSubscription> _streamSubscription; + final RefreshController refreshController = RefreshController(); + + final String appKey = 'kinetra'; + List purchaseList = []; + Function? func; + bool isCoinDialog = false; + bool isRaminVipDialog = false; // 是否挽回弹窗 + bool isCoinDialogOpen = false; + + // 会员金币弹窗是否容易点击 + bool coinsModalEasyClose = false; + bool forcedRecharge = false; + String closeLabel = ''; + Function? pauseVideoFunc; + Function? coinDialogCallback; + + @override + onReady() { + getStoreInfo(); + initPurchaseListener(); + super.onReady(); + } + + @override + void onClose() { + KtInAppPurchaseUtil.cancelStreamSubscription(_streamSubscription); + super.onClose(); + } + + /// 获取store商品列表 + getStoreInfo() async { + EasyLoading.show( + status: 'Searching...', + maskType: EasyLoadingMaskType.black, + dismissOnTap: false, + ); + try { + ApiResponse res = await KtHttpClient().request( + KtApis.paySettingsV4, + method: HttpMethod.get, + ); + EasyLoading.dismiss(); + refreshController.refreshCompleted(); + if (res.success) { + state.storeBean = KtStoreBean.fromJson(res.data); + update(); + initStore(); + } + } catch (e) { + EasyLoading.dismiss(); + } + } + + initStore() async { + EasyLoading.show(status: 'Searching...'); + bool isAvailable = await KtInAppPurchaseUtil.isAvailable(); + if (!isAvailable) { + return KtToastUtils.showError('In App purchase is not available'); + } + // 产品ID + List productIds = []; + // vipId + List vipIds = []; + if (Platform.isIOS) { + productIds = (state.storeBean.listCoins ?? []) + .map((item) => '$appKey.${item.iosTemplateId}') + .toList(); + vipIds = (state.storeBean.listSubVip ?? []) + .map((item) => '$appKey.${item.iosTemplateId}') + .toList(); + } else { + productIds = state.storeBean.listCoins! + .map((item) => item.androidTemplateId ?? '') + .toList(); + vipIds = state.storeBean.listSubVip! + .map((item) => item.androidTemplateId ?? '') + .toList(); + } + + if (productIds.isEmpty && vipIds.isEmpty) { + EasyLoading.dismiss(); + KtToastUtils.showError('Pay item is empty'); + return; + } + + ProductDetailsResponse productDetailsResponse = + await KtInAppPurchaseUtil.queryProducts({ + ...vipIds, + ...productIds, + }); + if (productDetailsResponse.error != null) { + EasyLoading.dismiss(); + + debugPrint( + "----productDetailsResponse.error${productDetailsResponse.error}", + ); + KtToastUtils.showError(productDetailsResponse.error!.message); + + return; + } + debugPrint('---未找到的商品: ${productDetailsResponse.notFoundIDs.join(', ')}'); + + debugPrint( + '----productDetailsResponse:${productDetailsResponse.productDetails.length}', + ); + if (productDetailsResponse.productDetails.isEmpty) { + EasyLoading.dismiss(); + + KtToastUtils.showError('Query store is empty'); + return; + } + if (Platform.isIOS) { + state.storeBean.listCoins!.removeWhere( + (a) => productDetailsResponse.productDetails.every((b) { + if (b.id == '$appKey.${a.iosTemplateId}') { + a.productDetails = b; + return false; + } + return true; + }), + ); + state.storeBean.listSubVip!.removeWhere( + (a) => productDetailsResponse.productDetails.every((b) { + if (b.id == '$appKey.${a.iosTemplateId}') { + a.productDetails = b; + return false; + } + return true; + }), + ); + } else if (Platform.isAndroid) { + state.storeBean.listCoins!.removeWhere( + (a) => productDetailsResponse.productDetails.every((b) { + if (b.id == a.androidTemplateId) { + a.productDetails = b; + return false; + } + return true; + }), + ); + state.storeBean.listSubVip!.removeWhere( + (a) => productDetailsResponse.productDetails.every((b) { + if (b.id == a.androidTemplateId) { + a.productDetails = b; + return false; + } + return true; + }), + ); + } + state.storeBean.listCoins?.sort((a, b) => a.size!.compareTo(b.size!)); + state.refillCoinList = + state.storeBean.listCoins + ?.where((item) => item.size == 'spread') + .toList() ?? + []; + EasyLoading.dismiss(); + + update(); + } + + /// 购买监听器 + initPurchaseListener() { + _streamSubscription = KtInAppPurchaseUtil.purchaseStream.listen( + (List purchaseDetailsList) { + purchaseList = purchaseDetailsList; + _listenToPurchaseUpdated(purchaseDetailsList); + }, + onDone: () { + _streamSubscription.cancel(); + }, + onError: (err) { + debugPrint('---stream-err:$err'); + }, + ); + } + + _listenToPurchaseUpdated(List purchaseDetailsList) async { + for (var purchaseDetails in purchaseDetailsList) { + debugPrint( + '---listen-purchaseDetails:${purchaseDetailsList.length} ${purchaseDetails.productID} ${purchaseDetails.status} ${purchaseDetails.pendingCompletePurchase}', + ); + + if (purchaseDetails.status == PurchaseStatus.pending) { + // 购买中 + debugPrint('Purchase is pending: ${purchaseDetails.productID}'); + KtInAppPurchaseUtil.completePurchase(purchaseDetails); + } else if (purchaseDetails.status == PurchaseStatus.purchased || + purchaseDetails.status == PurchaseStatus.restored) { + // 购买成功或恢复购买 + debugPrint( + 'Purchase successful: ${purchaseDetails.productID} ${purchaseDetails.status} ${purchaseDetails.pendingCompletePurchase}', + ); + + try { + KtGoodsBean? temp = + // isCoinDialog + // ? state.vipCoinStoreBean + // : isRaminVipDialog + // ? state.remainVipDialogBean + // : + [ + ...(state.storeBean.listCoins ?? []), + ...(state.storeBean.listSubVip ?? []), + ].firstWhereOrNull( + (item) => item.productDetails?.id == purchaseDetails.productID, + ); + if (temp == null) { + // UserUtil().reportErrorEvent( + // 'platform pay failed', + // UserUtil.payError, + // payData: {'id': purchaseDetails.productID, 'status': purchaseDetails.status}, + // errMsg: '未找到匹配商品', + // ); + KtToastUtils.showError('There were some problems with the payment'); + return; + } + KtGoodsBean? goods = KtGoodsBean.fromJson(temp.toJson()); + goods.transactionId = purchaseDetails.purchaseID; + goods.serverVerificationData = + purchaseDetails.verificationData.serverVerificationData; + EasyLoading.dismiss(); + if (KtUtils.isNotEmpty(goods.orderCode)) { + bool isSuccess = await verifyPay(goods); + if (isSuccess) KtInAppPurchaseUtil.consumeIfNeeded(purchaseDetails); + } + } catch (e) { + debugPrint('--purchase-success-err:$e'); + } + KtInAppPurchaseUtil.completePurchase(purchaseDetails); + } else if (purchaseDetails.status == PurchaseStatus.error) { + // 购买失败 + debugPrint('Purchase failed: ${purchaseDetails.error?.message}'); + EasyLoading.dismiss(); + KtGoodsBean? goods = + [ + ...(state.storeBean.listCoins ?? []), + ...(state.storeBean.listSubVip ?? []), + ].firstWhereOrNull( + (item) => item.productDetails?.id == purchaseDetails.productID, + ); + // UserUtil().reportErrorEvent( + // 'platform pay failed', + // UserUtil.payError, + // orderCode: goods?.orderCode ?? '', + // errMsg: purchaseDetails.error?.toString(), + // ); + debugPrint( + '-----itemAlreadyOwned:${purchaseDetails.error?.message.contains('itemAlreadyOwned')}', + ); + + if (purchaseDetails.error?.message.contains('itemAlreadyOwned') ?? + false) { + KtInAppPurchaseUtil.completePurchase(purchaseDetails, isRetry: true); + // KtGoodsBean? goods = [ + // ...(state.storeBean.listCoins ?? []), + // ...(state.storeBean.listSubVip ?? []), + // ].firstWhereOrNull((item) => item.productDetails?.id == purchaseDetails.productID); + // if (goods?.productDetails != null && count <= 2) { + // count++; + // KtInAppPurchaseUtil.buy(goods!.productDetails!); + // } + } else { + KtToastUtils.showError('There were some problems with the payment'); + } + } else if (purchaseDetails.status == PurchaseStatus.canceled) { + // 购买取消 + debugPrint('Purchase canceled: ${purchaseDetails.productID}'); + EasyLoading.dismiss(); + KtToastUtils.showToast('User cancel'); + // KtGoodsBean? goods = [ + // ...(state.storeBean.listCoins ?? []), + // ...(state.storeBean.listSubVip ?? []), + // ].firstWhereOrNull((item) => item.productDetails?.id == purchaseDetails.productID); + // UserUtil().reportErrorEvent('user pay canceled', "pay_cancel", orderCode: goods?.orderCode ?? ''); + } + KtInAppPurchaseUtil.completePurchase(purchaseDetails, isRetry: true); + } + } + + buyGoods(KtGoodsBean payItem, {num? shortPlayId, num? videoId}) { + if (payItem.buyType == 'sub_coins') { + // showSubCoinCheckDialog(payItem, shortPlayId: shortPlayId, videoId: videoId); + } else { + createOrder(payItem, shortPlayId: shortPlayId, videoId: videoId); + } + } + + createOrder(KtGoodsBean goods, {num? shortPlayId, num? videoId}) async { + EasyLoading.show(status: 'Paying...', maskType: EasyLoadingMaskType.black); + // Map params = {"pay_setting_id": goods.id!}; + Map params = { + "pay_setting_id": goods.id!, + "is_discount": 0, + }; + // if (Platform.isIOS && params["is_discount"] == 1) { + // final product = goods.productDetails as AppStoreProductDetails; + // params.putIfAbsent('product_discount', () => product.skProduct.discounts.first.identifier!); + // } + if (shortPlayId != null) { + params.putIfAbsent('short_play_id', () => shortPlayId); + state.shortPlayId = shortPlayId; + } + if (videoId != null) { + params.putIfAbsent('video_id', () => videoId); + state.videoId = videoId; + } + ApiResponse res = await KtHttpClient().request( + KtApis.createOrder, + data: params, + ); + EasyLoading.dismiss(); + if (res.success) { + if (res.data['code'] == 30007) { + KtToastUtils.showError( + isCoinDialog + ? 'You\'ve already subscribed to this coin pack' + : 'You are already VIP!', + ); + return; + } + KtOrderBean order = KtOrderBean.fromJson(res.data); + if (KtUtils.isEmpty(order.orderCode) || + KtUtils.isEmpty(goods.productDetails)) + return; + + goods.orderCode = order.orderCode; + state.orderCode = order.orderCode; + // if (isRaminVipDialog) state.remainVipDialogBean.orderCode = order.orderCode; + try { + // if (goods.discountType != 0) { + // if (Platform.isIOS) { + // if (goods.discountType == 2) { + // final product = goods.productDetails as AppStoreProductDetails; + // final purchaseParamSk2 = AppStorePurchaseParam( + // productDetails: goods.productDetails!, + // applicationUserName: order.discount!.signData!.applicationUsername!, + // discount: SKPaymentDiscountWrapper( + // identifier: product.skProduct.discounts.first.identifier!, + // keyIdentifier: order.discount!.signData!.keyIdentifier!, + // nonce: order.discount!.signData!.nonce!, + // signature: order.discount!.signData!.signature!, + // timestamp: order.discount!.signData!.timestamp!, + // ), + // ); + // debugPrint('----purchaseParamSk2 identifier:${purchaseParamSk2.discount?.identifier}'); + // await KtInAppPurchaseUtil.buyDiscount(purchaseParamSk2); + // } else { + // await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins'); + // } + // } else if (Platform.isAndroid) { + // if (goods.productDetails! is GooglePlayProductDetails) { + // final offer = + // (goods.productDetails! as GooglePlayProductDetails).productDetails.subscriptionOfferDetails!.first; + // + // debugPrint('----offertoken:${offer.offerIdToken}'); + // final purchaseParam = GooglePlayPurchaseParam( + // productDetails: goods.productDetails!, + // offerToken: offer.offerIdToken, + // ); + // await KtInAppPurchaseUtil.buyDiscount(purchaseParam); + // } + // } + // } else { + // if (Platform.isAndroid) { + // if (goods.productDetails! is GooglePlayProductDetails) { + // if ((goods.productDetails! as GooglePlayProductDetails).productDetails.subscriptionOfferDetails != null) { + // List offerList = + // (goods.productDetails! as GooglePlayProductDetails).productDetails.subscriptionOfferDetails!; + // final offerIdToken = offerList.length > 1 ? offerList[1].offerIdToken : offerList[0].offerIdToken; + // + // final purchaseParam = GooglePlayPurchaseParam( + // productDetails: goods.productDetails!, + // offerToken: offerIdToken, + // ); + // await KtInAppPurchaseUtil.buyDiscount(purchaseParam); + // } else { + // await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins'); + // } + // } else { + // await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins'); + // } + // } else { + // await KtInAppPurchaseUtil.buy(goods.productDetails!, consumable: goods.buyType == 'coins'); + // } + // } + await KtInAppPurchaseUtil.buy( + goods.productDetails!, + consumable: goods.buyType == 'coins', + ); + } catch (e) { + EasyLoading.dismiss(); + KtToastUtils.showError( + 'There were some problems with the payment,Please try again!', + ); + debugPrint('---e:$e'); + // UserUtil().reportErrorEvent( + // 'platform pay timeout', + // UserUtil.payPlatformTimeout, + // orderCode: goods.orderCode, + // errMsg: e.toString(), + // payData: params, + // ); + + KtInAppPurchaseUtil.clearFailedPurchases(); + + for (var item in purchaseList) { + if (item.productID == goods.productDetails?.id) { + KtInAppPurchaseUtil.completePurchase(item); + } + } + } + } else { + EasyLoading.dismiss(); + KtToastUtils.showError('Failed to create order'); + goods.orderCode = null; + } + } + + /// 校验订单 + Future verifyPay( + KtGoodsBean goods, { + bool isRestore = false, + bool isAuto = false, + }) async { + String url = Platform.isAndroid + ? KtApis.createGooglePay + : KtApis.createApplePay; + final deviceInfo = KtDeviceInfoUtil(); + String transactionId = goods.transactionId ?? ""; + String serverVerificationData = goods.serverVerificationData ?? ""; + Map params = { + 'pkg_name': deviceInfo.packageName ?? "", + 'order_code': goods.orderCode ?? "", + 'product_id': Platform.isAndroid + ? goods.androidTemplateId + : goods.iosTemplateId, + 'show_money': goods.price, + 'pay_setting_id': goods.id.toString(), + 'transaction_id': transactionId, + 'purchases_token': serverVerificationData, + }; + // if (isRestore) { + // UserUtil().reportErrorEvent( + // 'pay restore', + // UserUtil.payRestore, + // type: isAuto ? 'auto' : 'manual', + // orderCode: goods.orderCode, + // transactionId: transactionId, + // payData: params, + // ); + // } + + try { + ApiResponse res = await KtHttpClient().request(url, data: params); + if (res.success && res.data['status'] == 'success') { + if (!isRestore) { + KtToastUtils.showSuccess(placeholder: 'Pay Success'); + Future.delayed(Duration(seconds: 1), () => getStoreInfo()); + } + Get.put(KtMineLogic()).getUserInfo(); + + func?.call(); + // coinDialogCallback?.call(); + // if (isCoinDialog) { + // Get.back(); + // } + + ///todo:测试期间安卓所有商品先存本地,上线前删除本行 + // if (Platform.isAndroid) KtPurchaseRestoreUtil().cacheFailedGoods(goods); + KtPurchaseRestoreUtil().removeGoods(goods); + cleanFunc(); + return true; + } + coinDialogCallback?.call(); + // UserUtil().reportErrorEvent( + // 'pay callback failed', + // UserUtil.payCallback, + // orderCode: goods.orderCode, + // transactionId: transactionId, + // errMsg: params.toString(), + // ); + + if (!isRestore) KtPurchaseRestoreUtil().cacheFailedGoods(goods); + cleanFunc(); + return false; + } catch (e) { + coinDialogCallback?.call(); + if (!isRestore) KtPurchaseRestoreUtil().cacheFailedGoods(goods); + debugPrint('---e:$e'); + cleanFunc(); + return false; + } + } + + cleanFunc() { + func = null; + // coinDialogCallback = null; + // pauseVideoFunc = null; + // if (isCoinDialog) isCoinDialog = false; + } + + restorePay() {} +} diff --git a/lib/kt_pages/kt_mine/kt_store/state.dart b/lib/kt_pages/kt_mine/kt_store/state.dart new file mode 100644 index 0000000..f0cbb78 --- /dev/null +++ b/lib/kt_pages/kt_mine/kt_store/state.dart @@ -0,0 +1,17 @@ +import '../../../kt_model/kt_goods_bean.dart'; +import '../../../kt_model/kt_store_bean.dart'; + +class KtStoreState { + KtStoreBean storeBean = KtStoreBean(); + + List vipTypes = ['Monthly', 'Weekly', 'Quarterly', 'Yearly']; + List vipList = []; + List coinList = []; + List spreadCoinList = []; + List refillCoinList = []; + int selVip = 0; + int selCoin = 0; + String? orderCode; + num? shortPlayId; + num? videoId; +} diff --git a/lib/kt_pages/kt_mine/kt_store/view.dart b/lib/kt_pages/kt_mine/kt_store/view.dart index 7ffd1a7..1b344b9 100644 --- a/lib/kt_pages/kt_mine/kt_store/view.dart +++ b/lib/kt_pages/kt_mine/kt_store/view.dart @@ -1,4 +1,11 @@ +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_pages/kt_mine/kt_store/logic.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_kinetra/kt_widgets/kt_store_widget.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; class KtStorePage extends StatefulWidget { const KtStorePage({super.key}); @@ -8,10 +15,82 @@ class KtStorePage extends StatefulWidget { } class _KtStorePageState extends State { + final logic = Get.put(KtStoreLogic()); + final state = Get.find().state; + @override Widget build(BuildContext context) { - return const Scaffold( - body: Text('Store', style: TextStyle(color: Colors.black)), + return Container( + color: Color(0xFF1E1E20), + padding: EdgeInsets.fromLTRB(15.w, ScreenUtil().statusBarHeight, 15.w, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + child: Image.asset('ic_back_white.png'.ktIcon, width: 24.w), + onTap: () => Navigator.of(context).maybePop(), + ), + Text( + 'Store', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + GestureDetector( + onTap: () { + EasyThrottle.throttle( + 'restore-tap', + Duration(seconds: 3), + () => logic.restorePay(), + ); + }, + child: Text( + 'ReStore', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ], + ), + Expanded( + child: SmartRefresher( + controller: logic.refreshController, + physics: const ClampingScrollPhysics(), + enablePullUp: false, + enablePullDown: true, + onRefresh: logic.getStoreInfo, + child: SingleChildScrollView( + child: Column( + children: [ + GetBuilder( + assignId: true, + builder: (logic) { + return KtStoreWidget( + store: state.storeBean, + onItemTap: (goods) { + EasyThrottle.throttle( + 'buy', + Duration(seconds: 3), + () => logic.buyGoods(goods), + ); + }, + ); + }, + ), + ], + ), + ), + ), + ), + ], + ), ); } } diff --git a/lib/kt_pages/kt_mine/view.dart b/lib/kt_pages/kt_mine/view.dart index 9d5ef2c..82ffa7b 100644 --- a/lib/kt_pages/kt_mine/view.dart +++ b/lib/kt_pages/kt_mine/view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_pages/kt_mine/insert_web/kt_order_record_page.dart'; import 'package:flutter_kinetra/kt_pages/kt_mine/logic.dart'; import 'package:flutter_kinetra/kt_pages/kt_routes.dart'; import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; @@ -54,97 +55,157 @@ class _KtMinePageState extends State { onRefresh: logic.getUserInfo, child: Column( children: [ - Container( - width: ScreenUtil().screenWidth - 30.w, - margin: EdgeInsets.only(top: 22.w), - padding: EdgeInsets.fromLTRB(6.w, 13.w, 6.w, 6.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('ic_mine_top_bg.png'.ktIcon), - fit: BoxFit.fill, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Welcome Back, visitor!', - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - fontWeight: FontWeight.w500, + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: ScreenUtil().screenWidth - 30.w, + margin: EdgeInsets.only(top: 10.w), + padding: EdgeInsets.fromLTRB(6.w, 13.w, 6.w, 6.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('ic_mine_top_bg.png'.ktIcon), + fit: BoxFit.fill, ), ), - Container( - padding: EdgeInsets.symmetric( - vertical: 17.w, - horizontal: 13.w, - ), - margin: EdgeInsets.only(top: 13.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14.w), - ), - child: Row( - children: [ - Image.asset( - 'ic_avatar.png'.ktIcon, - width: 56.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Coins', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 5.w), + Image.asset( + 'ic_coin.png'.ktIcon, + width: 15.w, + ), + SizedBox(width: 2.w), + Text( + '${(state.userInfo.coinLeftTotal ?? 0) + (state.userInfo.sendCoinLeftTotal ?? 0)}', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + Container( + width: 1.w, + height: 10.w, + margin: EdgeInsets.symmetric( + horizontal: 15.w, + ), + color: Colors.white.withValues(alpha: .3), + ), + Text( + 'Donate', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 5.w), + Image.asset( + 'ic_coin.png'.ktIcon, + width: 15.w, + ), + SizedBox(width: 2.w), + Text( + '${state.userInfo.sendCoinLeftTotal ?? 0}', + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + + Container( + padding: EdgeInsets.symmetric( + vertical: 17.w, + horizontal: 13.w, ), - SizedBox(width: 13.w), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, + margin: EdgeInsets.only(top: 8.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14.w), + ), + child: Row( children: [ - Stack( - alignment: Alignment.bottomCenter, + Image.asset( + 'ic_avatar.png'.ktIcon, + width: 56.w, + ), + SizedBox(width: 13.w), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - Image.asset( - 'text_bg_discover.png'.ktIcon, - height: 17.w, - width: 55.w, - ), - Padding( - padding: EdgeInsets.only( - bottom: 2.w, - ), - child: Text( - KtUtils.isEmpty( - state.userInfo.familyName, - ) - ? 'Visitor' - : state - .userInfo - .familyName ?? - '', - style: TextStyle( - fontFamily: 'PingFang SC', - fontSize: 18.sp, - color: Colors.black, - fontWeight: FontWeight.w500, + Stack( + alignment: Alignment.bottomCenter, + children: [ + Image.asset( + 'text_bg_discover.png'.ktIcon, + height: 17.w, + width: 55.w, ), + Padding( + padding: EdgeInsets.only( + bottom: 2.w, + ), + child: Text( + KtUtils.isEmpty( + state.userInfo.familyName, + ) + ? 'Visitor' + : state + .userInfo + .familyName ?? + '', + style: TextStyle( + fontFamily: 'PingFang SC', + fontSize: 18.sp, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + Text( + 'ID: ${state.userInfo.customerId}', + style: TextStyle( + fontFamily: 'PingFang SC', + fontSize: 12.sp, + color: Color(0xFF5E5E5E), + fontWeight: FontWeight.w400, ), ), ], ), - Text( - 'ID: ${state.userInfo.customerId}', - style: TextStyle( - fontFamily: 'PingFang SC', - fontSize: 12.sp, - color: Color(0xFF5E5E5E), - fontWeight: FontWeight.w400, - ), - ), ], ), - ], - ), + ), + ], ), - ], - ), + ), + Positioned( + right: 0, + top: -28.w, + child: Image.asset('ic_mine_ip.png'.ktIcon,width: 80.w,height: 84.w)) + ], ), - SizedBox(height: 30.w), + SizedBox(height: 15.w), + vipView(), + SizedBox(height: 20.w), settingsView(), ], ), @@ -158,6 +219,124 @@ class _KtMinePageState extends State { ); } + Widget vipView() { + return GestureDetector( + onTap: () { + Get.toNamed(KtRoutes.store); + }, + child: Container( + width: ScreenUtil().screenWidth - 30.w, + padding: EdgeInsets.only(left: 20.w, top: 15.w, bottom: 12.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('mine_vip_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'VIP & Coins', + style: TextStyle( + color: Colors.black, + fontSize: 16.sp, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Exclusive Deals Inside', + style: TextStyle( + color: const Color(0xFF1E1E20).withValues(alpha: .5), + fontSize: 10.sp, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + ), + ], + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 3.w), + margin: EdgeInsets.only(left: 50.w), + decoration: BoxDecoration( + color: Color(0xFFA7F62F), + borderRadius: BorderRadius.circular(100), + border: Border.all(color: Color(0xFF487800)), + ), + child: Row( + children: [ + Text( + 'GO', + style: TextStyle( + color: Colors.black, + fontSize: 14.sp, + fontFamily: 'Inter', + fontWeight: FontWeight.w900, + ), + ), + SizedBox(width: 3.w), + Image.asset('ic_polygon.png'.ktIcon, width: 6.w), + ], + ), + ), + ], + ), + SizedBox(height: 8.w), + Container( + width: 208.w, + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 7.w), + decoration: BoxDecoration( + color: const Color(0x7FF2FFEA), + borderRadius: BorderRadius.circular(100), + border: Border.all(color: Colors.white, width: 1.w), + ), + child: Row( + children: [ + Image.asset('no_ad.png'.ktIcon, width: 12.w), + SizedBox(width: 2.w), + Text( + 'Ad-Free', + style: TextStyle(fontSize: 10.sp, color: Color(0xFF5E5E5E)), + ), + Container( + height: 8.w, + width: 1.w, + margin: EdgeInsets.symmetric(horizontal: 8.w), + color: Color(0xFFE6E6E6), + ), + Image.asset('full_access.png'.ktIcon, width: 12.w), + SizedBox(width: 2.w), + Text( + 'Full Access', + style: TextStyle(fontSize: 10.sp, color: Color(0xFF5E5E5E)), + ), + Container( + height: 8.w, + width: 1.w, + margin: EdgeInsets.symmetric(horizontal: 8.w), + color: Color(0xFFE6E6E6), + ), + Image.asset('ic_hd.png'.ktIcon, width: 12.w), + SizedBox(width: 2.w), + Text( + 'HD', + style: TextStyle(fontSize: 10.sp, color: Color(0xFF5E5E5E)), + ), + ], + ), + ), + ], + ), + ), + ); + } + Widget settingsView() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -185,6 +364,13 @@ class _KtMinePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ handleItem( + 'ic_order_record.png', + 'Order Record', + () => Get.to( + () => KtOrderRecordPage( + ), + ), + ), handleItem( 'ic_privacy.png', 'Privacy Policy', () => Get.to( diff --git a/lib/kt_pages/kt_my_list/view.dart b/lib/kt_pages/kt_my_list/view.dart index 8379b09..fce1d94 100644 --- a/lib/kt_pages/kt_my_list/view.dart +++ b/lib/kt_pages/kt_my_list/view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_kinetra/kt_model/kt_short_video_bean.dart'; import 'package:flutter_kinetra/kt_pages/kt_my_list/kt_my_chest_page.dart'; import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; import 'package:flutter_kinetra/kt_widgets/kt_status_widget.dart'; diff --git a/lib/kt_pages/kt_routes.dart b/lib/kt_pages/kt_routes.dart index 3c9a604..a3eee88 100644 --- a/lib/kt_pages/kt_routes.dart +++ b/lib/kt_pages/kt_routes.dart @@ -1,8 +1,8 @@ import 'package:flutter_kinetra/kt_pages/kt_home/kt_search_page.dart'; +import 'package:flutter_kinetra/kt_pages/kt_mine/insert_web/kt_order_record_page.dart'; import 'package:get/get_navigation/src/routes/get_route.dart'; import 'kt_main_page/view.dart'; -import 'kt_mine/insert_web/wallet_page.dart'; import 'kt_mine/kt_about_us_page.dart'; import 'kt_mine/kt_store/view.dart'; import 'kt_short_video/view.dart'; @@ -36,7 +36,7 @@ class KtRoutes { preventDuplicates: false, ), GetPage(name: store, page: () => KtStorePage()), - GetPage(name: wallet, page: () => const WalletPage()), + GetPage(name: wallet, page: () => const KtOrderRecordPage()), GetPage(name: aboutUs, page: () => const KtAboutUsPage()), // GetPage(name: helpCenter, page: () => const HelpCenterPage()), // GetPage(name: helpCenterList, page: () => const HelpCenterListPage()), diff --git a/lib/kt_pages/kt_short_video/logic.dart b/lib/kt_pages/kt_short_video/logic.dart index 7e83448..e4d9752 100644 --- a/lib/kt_pages/kt_short_video/logic.dart +++ b/lib/kt_pages/kt_short_video/logic.dart @@ -1,5 +1,11 @@ +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_pages/kt_mine/logic.dart'; import 'package:flutter_kinetra/kt_pages/kt_short_video/state.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_kinetra/kt_utils/kt_toast_utils.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:video_player/video_player.dart'; @@ -7,15 +13,19 @@ import '../../dio_cilent/kt_apis.dart'; import '../../dio_cilent/kt_request.dart'; import '../../kt_model/kt_video_detail_bean.dart'; import '../../kt_widgets/kt_status_widget.dart'; +import '../../kt_widgets/kt_store_widget.dart'; +import '../kt_mine/kt_store/logic.dart'; class VideoPlayLogic extends GetxController { final state = VideoPlayState(); late final PageController pageController; List controllers = []; - + KtLoadStatusType videoStatus = KtLoadStatusType.loading; + KtLoadStatusType loadStatusType = KtLoadStatusType.loadSuccess; int currentIndex = 0; bool _disposed = false; + final userLogic = Get.put(KtMineLogic()); @override void onInit() { @@ -60,7 +70,6 @@ class VideoPlayLogic extends GetxController { } clearCacheCtrl(); try { - // LottieLoading.show(Get.context!); ApiResponse res = await KtHttpClient().request( KtApis.getVideoDetails, method: HttpMethod.get, @@ -77,21 +86,17 @@ class VideoPlayLogic extends GetxController { } state.episodeList = state.video?.episodeList ?? []; if (toPage) currentIndex = (state.video?.videoInfo?.episode ?? 1) - 1; - // for (var video in state.episodeList) { - // if (video.isLock == true) { - // state.curUnlock = state.episodeList.indexOf(video); - // break; - // } - // } - // reportHistory(); + for (var video in state.episodeList) { + if (video.isLock == true) { + state.curUnlock = state.episodeList.indexOf(video); + break; + } + } controllers = List.filled( state.episodeList.length, null, growable: true, ); - // await _initializeController(currentIndex); - // _preloadAdjacentVideos(); - // if(currentIndex != 0){ if (toPage) { onPageChanged( currentIndex, @@ -101,17 +106,15 @@ class VideoPlayLogic extends GetxController { } else { _initializeController(currentIndex); } - // } update(); } else { state.loadStatus = KtLoadStatusType.loadFailed; update(); } - // if (userLogic.state.userInfo.isVip == true) state.curUnlock = 9999; + if (userLogic.state.userInfo.isVip == true) state.curUnlock = 9999; update(); } catch (e) { - // LottieLoading.close(); state.loadStatus = KtLoadStatusType.loadFailed; update(); } @@ -149,42 +152,49 @@ class VideoPlayLogic extends GetxController { // 暂停当前视频 if (controllers[currentIndex]?.value.isPlaying ?? false) { await controllers[currentIndex]?.pause(); - // if (isUploadHistorySeconds) uploadHistorySeconds(controllers[currentIndex]?.value.position.inMilliseconds ?? 0); + if (isUploadHistorySeconds) { + uploadHistorySeconds( + controllers[currentIndex]?.value.position.inMilliseconds ?? 0, + ); + } } if (controllers[currentIndex]?.value.isCompleted ?? false) { - // if (isUploadHistorySeconds) uploadHistorySeconds(0); + if (isUploadHistorySeconds) uploadHistorySeconds(0); if (state.activityId != null) reportActivity(); } if (isToggle) { - // loadStatusType = LoadStatusType.loading; + loadStatusType = KtLoadStatusType.loading; update(); await _initializeController(index); - // loadStatusType = LoadStatusType.loadSuccess; + loadStatusType = KtLoadStatusType.loadSuccess; update(); WidgetsBinding.instance.addPostFrameCallback((_) { pageController.jumpToPage(index); }); } currentIndex = index; - // if (state.episodeList[index].isLock == true) { - // controllers[index]?.seekTo(Duration(seconds: 0)); - // controllers[index]?.pause(); - // update(); - // // showUnlockDialog(); - // // - // return; - // } + if (state.episodeList[index].isLock == true) { + controllers[index]?.seekTo(Duration(seconds: 0)); + controllers[index]?.pause(); + update(); + // EasyThrottle.throttle( + // 'show-buy-dialog', + // Duration(milliseconds: 3000), + // () => showStoreDialog(state.episodeList[index].coins ?? 0), + // ); + return; + } if (controllers[index] != null) { - // if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) { - controllers[index]?.play(); - // } + if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) { + controllers[index]?.play(); + } } else { if (!isToggle) await _initializeController(index); - // if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) { - controllers[index]?.play(); - // } + if (state.curUnlock > index || userLogic.state.userInfo.isVip == true) { + controllers[index]?.play(); + } } controllers[index]?.setPlaybackSpeed(state.currentSpeed); // updateHomeVideo(); @@ -221,8 +231,7 @@ class VideoPlayLogic extends GetxController { try { await controller.initialize(); - // if (index == currentIndex && (episode.isLock == false || userLogic.state.userInfo.isVip == true)) { - if (index == currentIndex) { + if (index == currentIndex && (episode.isLock == false || userLogic.state.userInfo.isVip == true)) { controller.play(); update(); } @@ -261,6 +270,15 @@ class VideoPlayLogic extends GetxController { _releaseUnusedControllers(); } + void uploadHistorySeconds(int playSeconds) { + Map params = { + "short_play_id": state.shortPlayId, + "video_id": state.episodeList[currentIndex].id, + "play_seconds": playSeconds > 0 ? playSeconds : 0, + }; + KtHttpClient().request(KtApis.uploadHistorySeconds, data: params); + } + Future likeVideo() async { if (state.video == null) return; @@ -276,18 +294,128 @@ class VideoPlayLogic extends GetxController { state.video?.shortPlayInfo?.isCollect = !(state.video?.shortPlayInfo?.isCollect ?? false); - // update(['video-like']); - // if (state.isFromDiscover) { - // try { - // final discoverLogic = Get.put(DiscoverLogic()); - // discoverLogic.setCollectVideo( - // state.video!.shortPlayInfo.shortPlayId!, - // isCollect: state.video!.shortPlayInfo.isCollect!, - // ); - // } catch (e) { - // debugPrint('---err:$e'); - // } - // } update(); } + + // 购买剧集 + buyVideo(num videoId, num coins, {bool toRecharge = false}) async { + ApiResponse res = await KtHttpClient().request( + KtApis.buyVideo, + data: {"short_play_id": state.shortPlayId, "video_id": videoId}, + ); + if (res.success) { + String status = res.data['status']; + if (status == 'success') { + int index = state.episodeList.indexWhere((item) => item.id == videoId); + state.episodeList[index].isLock = false; + if (index != state.episodeList.length - 1) { + state.curUnlock = index + 1; + } + update(); + onPageChanged(index, isToggle: true); + final mineLogic = Get.put(KtMineLogic()); + mineLogic.getUserInfo(); + } else if (res.data['status'] == 'not_enough') { + KtToastUtils.showError('Coin not enough'); + if (toRecharge) showStoreDialog(coins); + // if (toRecharge) state.detailBean?.payMode == 1 ? showBuyCoinDialog(coins) : showVipBuyDialog(coins); + } else if (status == 'jump') { + KtToastUtils.showError('Cannot jump episode'); + } + } else { + KtToastUtils.showError('Purchase failed'); + } + } + + showStoreDialog(num coins) { + Get.put(KtStoreLogic()); + // EasyThrottle.throttle('restore-short', Duration(seconds: 3), () => BuyUtils.restorePay(showTips: false)); + Get.bottomSheet( + isScrollControlled: true, + Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight - kToolbarHeight, + padding: EdgeInsets.fromLTRB(15.w, 72.w, 15.w, 10.w), + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage('recharge_bg.png'.ktIcon)), + ), + child: SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + + children: [ + Text( + 'Balance', + style: TextStyle(fontSize: 12.sp, color: Colors.black), + ), + Image.asset('ic_coin.png'.ktIcon, width: 20.w), + GetBuilder( + builder: (logic) { + return Text( + '${(logic.state.userInfo.coinLeftTotal ?? 0) + (logic.state.userInfo.sendCoinLeftTotal ?? 0)}', + style: TextStyle(fontSize: 13.sp, color: Colors.black), + ); + }, + ), + SizedBox(width: 2.w), + Image.asset('ic_coin.png'.ktIcon, width: 16.w), + Container( + width: 1.w, + height: 10.w, + margin: EdgeInsets.symmetric(horizontal: 18.w), + color: Color(0xFF5E5E5E), + ), + Text( + 'Unlock:', + style: TextStyle(fontSize: 12.sp, color: Colors.black), + ), + Image.asset('ic_coin.png'.ktIcon, width: 20.w), + SizedBox(width: 2.w), + Text( + '$coins', + style: TextStyle( + fontSize: 13.sp, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + GetBuilder( + builder: (logic) { + return KtStoreWidget( + store: logic.state.storeBean, + isStoreDialog: true, + onItemTap: (goods) async { + logic.func = () { + Get.back(); + fetchData(toPage: false); + buyVideo( + state.episodeList[currentIndex].id!, + coins, + toRecharge: false, + ); + // checkVipCoinSub(); + }; + EasyThrottle.throttle( + 'buy', + Duration(seconds: 3), + () async => await logic.buyGoods( + goods, + shortPlayId: state.shortPlayId, + videoId: state.episodeList[currentIndex].id, + ), + ); + }, + ); + }, + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/kt_pages/kt_short_video/state.dart b/lib/kt_pages/kt_short_video/state.dart index f792658..e22ffd4 100644 --- a/lib/kt_pages/kt_short_video/state.dart +++ b/lib/kt_pages/kt_short_video/state.dart @@ -5,6 +5,7 @@ class VideoPlayState { String imageUrl = ''; int shortPlayId = -1; num videoId = -1; + int curUnlock = 999; int? activityId; bool isFromDiscover = false; VideoDetailBean? video; diff --git a/lib/kt_pages/kt_short_video/view.dart b/lib/kt_pages/kt_short_video/view.dart index 6b9e796..d5ce617 100644 --- a/lib/kt_pages/kt_short_video/view.dart +++ b/lib/kt_pages/kt_short_video/view.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; import 'package:flutter_kinetra/kt_utils/kt_utils.dart'; @@ -8,6 +9,7 @@ import 'package:get/get.dart'; import 'package:video_player/video_player.dart'; import 'package:visibility_detector/visibility_detector.dart'; +import '../../kt_utils/kt_toast_utils.dart'; import '../../kt_widgets/kt_network_image.dart'; import '../../kt_widgets/kt_status_widget.dart'; import '../../kt_widgets/kt_video_progress_bar.dart'; @@ -128,15 +130,14 @@ class _VideoPlayPageState extends State if (logic.controllers.isEmpty) return Container(); final controller = logic.controllers[index]; - // final episode = state.episodeList[index]; + final episode = state.episodeList[index]; return Stack( children: [ // 视频播放器 if (controller != null && controller.value.isInitialized && !isAllOver) GestureDetector( onTap: () { - // if (episode.isLock == true) return; - + if (episode.isLock == true) return; controller.value.isPlaying ? controller.pause() : controller.play(); @@ -153,7 +154,7 @@ class _VideoPlayPageState extends State onVisibilityChanged: (VisibilityInfo info) { var visiblePercentage = info.visibleFraction * 100; if (visiblePercentage > 20) { - // if (episode.isLock == true) return; + if (episode.isLock == true) return; controller.play(); } else { controller.pause(); @@ -201,48 +202,89 @@ class _VideoPlayPageState extends State ], ), - // if (episode.isLock == true) - // Container( - // width: ScreenUtil().screenWidth, - // height: ScreenUtil().screenHeight, - // color: Colors.black.withValues(alpha: .75), - // child: Center( - // child: GestureDetector( - // onTap: () { - // EasyThrottle.throttle( - // 'unlock', - // Duration(seconds: 2), - // () => logic.buyVideo( - // episode.id!, - // episode.coins ?? 0, - // toRecharge: true, - // ), - // ); - // }, - // child: Container( - // width: 260.w, - // padding: EdgeInsets.symmetric(vertical: 17.w), - // decoration: MyStyles.mainBorder(width: 1, radius: 50), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Image.asset('ic_unlock_lock.png'.ktIcon, width: 20.w), - // SizedBox(width: 3.w), - // Text( - // state.curUnlock == index - // ? 'Unlocking costs ${episode.coins ?? 0} coins' - // : 'Prev.locked', - // style: TextStyle( - // fontSize: 14.sp, - // color: DrColors.mainWhite, - // ), - // ), - // ], - // ), - // ), - // ), - // ), - // ), + if (episode.isLock == true) + Stack( + children: [ + KtNetworkImage( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + imageUrl: state.imageUrl, + placeholder: Image.asset( + 'bg2.png'.ktIcon, + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + ), + Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + color: Colors.black.withValues(alpha: .75), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('lock_white.png'.ktIcon, width: 50.w), + SizedBox(height: 26.w), + GestureDetector( + onTap: () { + EasyThrottle.throttle( + 'unlock', + Duration(seconds: 2), + () => logic.buyVideo( + episode.id!, + episode.coins ?? 0, + toRecharge: true, + ), + ); + }, + child: Container( + width: 280.w, + padding: EdgeInsets.only(top: 17.w, bottom: 28.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('unlock_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('lock_black.png'.ktIcon, width: 20.w), + SizedBox(width: 3.w), + Text( + state.curUnlock == index + ? 'Unlocking costs ${episode.coins ?? 0}' + : 'Prev.locked', + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFF1E1E20), + ), + ), + Image.asset('ic_coin.png'.ktIcon, width: 18.7.w), + + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Balance:${logic.userLogic.state.userInfo.coinLeftTotal ?? 0} ", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFF5F5F5), + ), + ), + Image.asset('ic_coin.png'.ktIcon, width: 11.2.w), + + ], + ), + ], + ), + ), + ], + ), // 顶部返回 Positioned( top: ScreenUtil().statusBarHeight + 10.w, @@ -623,11 +665,13 @@ class _VideoPlayPageState extends State return GestureDetector( onTap: () { Get.back(); - // bool? beforeEpUnlock = index - 2 <= 0 ? false : state.episodeList[index - 2].isLock; - // if (beforeEpUnlock ?? false) { - // ToastUtils.show('Unable to unlock episodes'); - // return; - // } + bool? beforeEpUnlock = index - 1 <= 0 + ? false + : state.episodeList[index - 1].isLock; + if (beforeEpUnlock ?? false) { + KtToastUtils.showError('Unable to unlock episodes'); + return; + } logic.onPageChanged(index, isToggle: true); }, child: Stack( @@ -654,27 +698,15 @@ class _VideoPlayPageState extends State ), ), ), - // if (state.episodeList[episode - 1].isLock == true) - // Positioned( - // right: 0, - // top: 0, - // child: Container( - // width: 20.w, - // height: 12.w, - // decoration: BoxDecoration( - // color: ColorResource.mainYellow, - // borderRadius: BorderRadius.only( - // topRight: Radius.circular(6.w), - // bottomLeft: Radius.circular(6.w), - // ), - // ), - // child: Image.asset( - // 'ic_video_lock.png'.icon, - // width: 10.w, - // height: 10.w, - // ), - // ), - // ), + if (state.episodeList[index].isLock == true) + Positioned( + right: 5.w, + top: 5.w, + child: Image.asset( + 'ic_lock.png'.ktIcon, + width: 12.w, + ), + ), ], ), ); diff --git a/lib/kt_utils/kt_iap_util.dart b/lib/kt_utils/kt_iap_util.dart new file mode 100644 index 0000000..d5b7efb --- /dev/null +++ b/lib/kt_utils/kt_iap_util.dart @@ -0,0 +1,127 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; + +// 内购工具类 +class KtInAppPurchaseUtil { + static final InAppPurchase _iap = InAppPurchase.instance; + + /// 查询可用商品 + static Future queryProducts(Set productIds) async { + return await _iap.queryProductDetails(productIds); + } + + /// 购买商品(自动区分消耗型/非消耗型) + static Future buy(ProductDetails product, {bool consumable = false}) async { + final purchaseParam = PurchaseParam(productDetails: product); + if (consumable) { + await _iap.buyConsumable(purchaseParam: purchaseParam, autoConsume: Platform.isIOS); + } else { + await _iap.buyNonConsumable(purchaseParam: purchaseParam); + } + } + + /// 监听购买更新 + static Stream> get purchaseStream => _iap.purchaseStream; + + /// 完成购买(消耗型商品) + static Future completePurchase(PurchaseDetails purchase, {bool isRetry = false}) async { + if (purchase.pendingCompletePurchase) { + if (isRetry) { + for (int i = 0; i < 3; i++) { + try { + await _iap.completePurchase(purchase); + return; + } catch (e) { + debugPrint('---err:${e}'); + } + } + } else { + await _iap.completePurchase(purchase); + } + } + + // if (Platform.isAndroid) { + // final InAppPurchaseAndroidPlatformAddition addition = + // InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; + // if (purchase.productID.contains('coins')) { + // await addition.consumePurchase(purchase); + // } + // QueryPurchaseDetailsResponse response = await addition.queryPastPurchases(); + // for (GooglePlayPurchaseDetails purchaseDetails in response.pastPurchases) { + // _iap.completePurchase(purchaseDetails); + // } + // } else { + // if (purchase.pendingCompletePurchase) { + // await _iap.completePurchase(purchase); + // } + // } + } + + static Future consumeIfNeeded(PurchaseDetails purchaseDetails) async { + if (purchaseDetails is GooglePlayPurchaseDetails) { + final InAppPurchaseAndroidPlatformAddition addition = + InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; + await addition.consumePurchase(purchaseDetails); + } + } + + /// 检查是否可用 + static Future isAvailable() async { + return await _iap.isAvailable(); + } + + /// 恢复购买(非消耗型商品) + static Future restorePurchases() async { + await _iap.restorePurchases(); + } + + /// 校验购买状态 + static bool isPurchaseSuccess(PurchaseDetails purchase) { + return purchase.status == PurchaseStatus.purchased || purchase.status == PurchaseStatus.restored; + } + + /// 取消订阅流(如需手动管理) + static void cancelStreamSubscription(StreamSubscription? sub) { + sub?.cancel(); + } + + /// 完成错误的购买 + // static void completeFailed() { + // _iap.clearPendingPurchases(); + // } + + // @visibleForTesting + static Future clearFailedPurchases() async { + if (Platform.isIOS || Platform.isMacOS) { + final wrapper = SKPaymentQueueWrapper(); + final transactions = await wrapper.transactions(); + for (final transaction in transactions) { + // if (transaction.transactionState == SKPaymentTransactionStateWrapper.failed) { + await wrapper.finishTransaction(transaction); + // } + } + } + } + + /// 获取未完成的购买(通过监听 purchaseStream) + static Future> getPendingPurchases() async { + final available = await isAvailable(); + if (!available) return []; + final List pending = []; + // 只获取一次当前流数据 + final completer = Completer>(); + final sub = purchaseStream.listen((purchases) { + pending.addAll(purchases.where((p) => p.status == PurchaseStatus.pending)); + completer.complete(pending); + }); + final result = await completer.future; + await sub.cancel(); + return result; + } +} \ No newline at end of file diff --git a/lib/kt_utils/kt_purchase_restore_utils.dart b/lib/kt_utils/kt_purchase_restore_utils.dart new file mode 100644 index 0000000..aa52ff6 --- /dev/null +++ b/lib/kt_utils/kt_purchase_restore_utils.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../kt_model/kt_goods_bean.dart'; + +class KtPurchaseRestoreUtil { + static final KtPurchaseRestoreUtil _instance = KtPurchaseRestoreUtil._internal(); + + factory KtPurchaseRestoreUtil() => _instance; + + KtPurchaseRestoreUtil._internal(); + + static const String _cacheKey = 'restore_goods_list'; + + /// 保存失败的goods到本地缓存 + Future cacheFailedGoods(KtGoodsBean goods) async { + final prefs = await SharedPreferences.getInstance(); + List goodsList = prefs.getStringList(_cacheKey) ?? []; + goodsList.add(json.encode(goods.toJson())); + print('-----goods.toJson:${goods.toJson()}'); + await prefs.setStringList(_cacheKey, goodsList); + } + + /// 读取本地缓存的goods列表 + Future> getCachedGoodsList() async { + final prefs = await SharedPreferences.getInstance(); + List goodsList = prefs.getStringList(_cacheKey) ?? []; + return goodsList.map((e) => KtGoodsBean.fromJson(json.decode(e))).toList(); + } + + /// 清除本地缓存 + Future clearCachedGoods() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_cacheKey); + } + + /// 移除指定goods + Future removeGoods(KtGoodsBean goods) async { + final prefs = await SharedPreferences.getInstance(); + List goodsList = prefs.getStringList(_cacheKey) ?? []; + goodsList.removeWhere((e) { + final g = KtGoodsBean.fromJson(json.decode(e)); + return g.id == goods.id; + }); + await prefs.setStringList(_cacheKey, goodsList); + } +} diff --git a/lib/kt_utils/kt_toast_utils.dart b/lib/kt_utils/kt_toast_utils.dart index 027039d..1714e46 100644 --- a/lib/kt_utils/kt_toast_utils.dart +++ b/lib/kt_utils/kt_toast_utils.dart @@ -9,7 +9,7 @@ final class KtToastUtils { static CancelFunc showError(String msg) { return showToast( msg, - icon: Icon(Icons.error, size: 13, color: Colors.white), + icon: Icon(Icons.error, size: 13, color: Colors.black), ); } diff --git a/lib/kt_widgets/kt_store_widget.dart b/lib/kt_widgets/kt_store_widget.dart new file mode 100644 index 0000000..fcab6b9 --- /dev/null +++ b/lib/kt_widgets/kt_store_widget.dart @@ -0,0 +1,364 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_kinetra/kt_model/kt_goods_bean.dart'; +import 'package:flutter_kinetra/kt_utils/kt_string_extend.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../kt_model/kt_store_bean.dart'; + +class KtStoreWidget extends StatefulWidget { + final KtStoreBean store; + final bool onlyCoins; + final bool isStoreDialog; + final void Function(KtGoodsBean goods)? onItemTap; // 新增回调 + + const KtStoreWidget({ + super.key, + required this.store, + this.onItemTap, + this.isStoreDialog = false, + this.onlyCoins = false, + }); + + @override + State createState() => _KtStoreWidgetState(); +} + +class _KtStoreWidgetState extends State { + int selVip = -1; + int selCoin = -1; + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: initWidget(), + ); + } + + Widget descView() { + return Text(''' +1. Coins are virtual items and cannot be refunded. Use it for this product. \n +2. Gold coins will never expire, the reward coins will expire 24 hours a day.\n +3. Coins will be used first when unlocking episodes. If the amount is insufficient, reward coins will automatically be used. \n +4. The purchase has not been credited, click to refresh. \n +5. For other questions, contact us via Profile>Help &feedback.\n + ''', style: TextStyle(fontSize: 12.sp, color: Color(0xFF848484),height: 0.95)); + } + + List initWidget() { + // if (widget.onlyCoins) return [coinList()]; + List widgets = [SizedBox(height: 10.w)]; + // + // int coinsIndex = widget.store.sort?.indexOf('list_coins') ?? 0; + // int vipIndex = widget.store.sort?.indexOf('list_sub_vip') ?? 0; + // if (coinsIndex < vipIndex) { + // widgets.addAll([coinList(), SizedBox(height: 15.w), vipList()]); + // } else { + // widgets.addAll([vipList(), SizedBox(height: 15.w), coinList()]); + // } + widgets.addAll([coinList()]); + + widgets.addAll([ + if (widget.store.payMode == 0 && widget.store.showType == 0) + SizedBox(height: 15.w), + descView(), + ]); + + return widgets; + } + + List get smallList => + widget.store.listCoins!.where((item) => item.size == 'small').toList(); + + List get bigList => + widget.store.listCoins!.where((item) => item.size == 'big').toList(); + + Widget coinList() { + if (widget.store.listCoins?.isEmpty ?? true) return Container(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!widget.onlyCoins) + Row( + children: [ + Text( + 'Go Coins', + style: TextStyle( + fontSize: 14.sp, + color:widget.isStoreDialog? Color(0xFF1E1E20):Colors.white, + fontWeight: FontWeight.w600, + ), + ), + Text( + ' | Limited-time coin packs', + style: TextStyle( + fontSize: 14.sp, + color:widget.isStoreDialog? Color(0xFF1E1E20):Colors.white, + fontWeight: FontWeight.w200, + ), + ), + ], + ), + SizedBox(height: 10.w), + // big + // if (!onlyNewSubCoins) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...bigList.map((item) { + return GestureDetector( + onTap: () { + selCoin = item.id ?? -1; + setState(() {}); + widget.onItemTap?.call(item); // 回调 + }, + child: Stack( + children: [ + Container( + width: 167.w, + padding: EdgeInsets.fromLTRB(4.w, 17.w, 5.w, 3.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('coin_big_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'ic_coin.png'.ktIcon, + width: 22.4.w, + ), + Text( + '${item.coins ?? 0}', + style: TextStyle( + fontSize: 20.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 4.w), + if ((item.sendCoins ?? 0) > 0) + Text( + '+${item.sendCoins}', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Color(0xFFAD8502), + ), + ), + ], + ), + SizedBox(height: 6.w), + Container( + width: 160.w, + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 6.w), + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular(8.w), + ), + child: Text( + '${item.productDetails?.price ?? 0}', + style: TextStyle( + fontSize: 13.sp, + color: Color(0xFFFFD321), + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ), + if (!item.cornerMarker.isNullString) + Positioned( + top: 4.w, + left: 4.w, + child: Image.asset( + 'ic_fire.png'.ktIcon, + width: 16.w, + ), + ), + if ((item.sendCoins ?? 0) > 0) + Positioned( + right: 0, + top: 0, + child: Container( + width: 42.w, + height: 16.w, + padding: EdgeInsets.symmetric(horizontal: 3.w), + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment(0.50, 0.00), + end: Alignment(0.50, 1.00), + colors: [ + const Color(0xFFFD9A68), + const Color(0xFFFF538D), + ], + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.w), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + ), + ), + child: Text( + '+${((item.sendCoins ?? 0) / (item.coins ?? 1) * 100).toInt()}%', + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + }), + ], + ), + SizedBox(height: 10.w), + // if (!widget.onlyCoins) subCoinList(), + // small + // if (!onlyNewSubCoins) + GridView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 10.w, + crossAxisSpacing: 10.w, + childAspectRatio: 108 / 90, + ), + itemBuilder: (_, index) { + var item = smallList[index]; + return GestureDetector( + onTap: () { + selCoin = item.id ?? -1; + setState(() {}); + widget.onItemTap?.call(item); // 回调 + }, + child: Stack( + children: [ + Container( + width: 108.w, + padding: EdgeInsets.fromLTRB(5.w, 14.w, 5.w, 4.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('coin_small_bg.png'.ktIcon), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'ic_coin.png'.ktIcon, + width: 22.4.w, + ), + Text( + '${item.coins ?? 0}', + style: TextStyle( + fontSize: 20.sp, + color: Color(0xFF1E1E20), + fontWeight: FontWeight.w500, + height: 0.8, + ), + ), + ], + ), + if ((item.sendCoins ?? 0) > 0) + Text( + '+${item.sendCoins}', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Color(0xFF499C00), + ), + ), + ], + ), + const Spacer(), + Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 4.w), + decoration: BoxDecoration( + color: Color(0xFF1E1E20), + borderRadius: BorderRadius.circular(8.w), + ), + child: Text( + '${item.productDetails?.price ?? 0}', + style: TextStyle( + fontSize: 13.sp, + color: Color(0xFF80FF00), + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ), + if (!item.cornerMarker.isNullString) + Positioned( + top: 4.w, + left: 4.w, + child: Image.asset('ic_fire.png'.ktIcon, width: 16.w), + ), + if ((item.sendCoins ?? 0) > 0) + Positioned( + right: 0, + top: 0, + child: Container( + width: 42.w, + height: 16.w, + padding: EdgeInsets.symmetric(horizontal: 3.w), + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment(0.50, 0.00), + end: Alignment(0.50, 1.00), + colors: [ + const Color(0xFFFD9A68), + const Color(0xFFFF538D), + ], + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.w), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + ), + ), + child: Text( + '+${((item.sendCoins ?? 0) / (item.coins ?? 1) * 100).toInt()}%', + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + }, + itemCount: smallList.length, + ), + ], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index dd00b44..3671fe6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,7 +10,6 @@ import 'package:wakelock_plus/wakelock_plus.dart'; import 'kt_pages/kt_routes.dart'; import 'kt_utils/kt_device_info_utils.dart'; -import 'kt_utils/kt_keys.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized();