feat:iOS一期

This commit is contained in:
zengyi 2025-09-23 15:09:18 +08:00
parent 82d17e7dd8
commit ab2e1a49f9
30 changed files with 2775 additions and 292 deletions

View File

@ -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

View File

@ -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")
}
}
}

BIN
assets/ic_lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

BIN
assets/ic_order_record.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

BIN
assets/ic_polygon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

BIN
assets/lock_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

BIN
assets/lock_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/recharge_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

BIN
assets/unlock_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -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";

View File

@ -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<String, dynamic> toJson() {
final map = <String, dynamic>{};
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<String>? subCoinsTxtList,
List<BackhaulPercentConf>? 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<String>() : [];
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<String>? _subCoinsTxtList;
List<BackhaulPercentConf>? _backhaulPercentConf;
ExtInfo copyWith({
String? size,
int? extraCoins,
int? maxTotalCoins,
int? maxTotalCoinsPop,
String? receiveCoinsRate,
int? extraDayCoins,
bool? coinsWinBackShow,
bool? isVipShow,
List<String>? subCoinsTxtList,
List<BackhaulPercentConf>? 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<String>? get subCoinsTxtList => _subCoinsTxtList;
List<BackhaulPercentConf>? get backhaulPercentConf => _backhaulPercentConf;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
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<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['episode_num'] = _episodeNum;
map['backhaul_percent'] = _backhaulPercent;
return map;
}
}

View File

@ -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<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['order_code'] = _orderCode;
map['money'] = _money;
map['is_backhaul'] = _isBackhaul;
return map;
}
}

View File

@ -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<String>? sort,
List<KtGoodsBean>? listVip,
List<KtGoodsBean>? listCoins,
List<KtGoodsBean>? listSubCoins,
List<KtGoodsBean>? listSubVip,
List<KtGoodsBean>? 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<String>() : [];
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<String>? _sort;
List<KtGoodsBean>? _listVip;
List<KtGoodsBean>? _listCoins;
List<KtGoodsBean>? _listSubCoins;
List<KtGoodsBean>? _listSubVip;
List<KtGoodsBean>? _listRetrieve;
KtStoreBean copyWith({
int? payMode,
int? showType,
List<String>? sort,
List<KtGoodsBean>? listVip,
List<KtGoodsBean>? listCoins,
List<KtGoodsBean>? listSubCoins,
List<KtGoodsBean>? listSubVip,
List<KtGoodsBean>? 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<String>? get sort => _sort;
List<KtGoodsBean>? get listVip => _listVip;
List<KtGoodsBean>? get listCoins => _listCoins;
List<KtGoodsBean>? get listSubCoins => _listSubCoins;
List<KtGoodsBean>? get listSubVip => _listSubVip;
List<KtGoodsBean>? get listRetrieve => _listRetrieve;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
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());

View File

@ -154,7 +154,7 @@ class SignInActivityPageState extends State<KtSearchPage> 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<KtSearchPage> with RouteAware {
}
Widget _buildWidget() {
print('----loadStatus:$loadingStatus');
debugPrint('----loadStatus:$loadingStatus');
switch (loadingStatus) {
case KtLoadStatusType.loading:
return Center(child: CircularProgressIndicator());

View File

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

View File

@ -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<WalletPage> with RouteAware {
class KtOrderRecordPageState extends State<KtOrderRecordPage> with RouteAware {
InAppWebViewController? _webViewController;
late PullToRefreshController _webRefreshController;
late Map<String, String> _userData;
// LoadStatusType loadingStatus = LoadStatusType.loading;
KtLoadStatusType loadingStatus = KtLoadStatusType.loading;
@override
void initState() {
@ -39,10 +40,10 @@ class SignInActivityPageState extends State<WalletPage> 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<WalletPage> 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<WalletPage> with RouteAware {
return window.flutter_inappwebview.callHandler('js2app',jsonS);
}
};
""",
injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
),
@ -167,37 +160,26 @@ class SignInActivityPageState extends State<WalletPage> 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<WalletPage> 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<WalletPage> 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();
}
}
}

View File

@ -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<List<PurchaseDetails>> _streamSubscription;
final RefreshController refreshController = RefreshController();
final String appKey = 'kinetra';
List<PurchaseDetails> 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<String> productIds = [];
// vipId
List<String> 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(<String>{
...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<PurchaseDetails> purchaseDetailsList) {
purchaseList = purchaseDetailsList;
_listenToPurchaseUpdated(purchaseDetailsList);
},
onDone: () {
_streamSubscription.cancel();
},
onError: (err) {
debugPrint('---stream-err:$err');
},
);
}
_listenToPurchaseUpdated(List<PurchaseDetails> 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<String, dynamic> params = {"pay_setting_id": goods.id!};
Map<String, dynamic> 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<SubscriptionOfferDetailsWrapper> 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<bool> 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<String, dynamic> 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() {}
}

View File

@ -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<KtGoodsBean> vipList = [];
List<KtGoodsBean> coinList = [];
List<KtGoodsBean> spreadCoinList = [];
List<KtGoodsBean> refillCoinList = [];
int selVip = 0;
int selCoin = 0;
String? orderCode;
num? shortPlayId;
num? videoId;
}

View File

@ -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<KtStorePage> {
final logic = Get.put(KtStoreLogic());
final state = Get.find<KtStoreLogic>().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<KtStoreLogic>(
assignId: true,
builder: (logic) {
return KtStoreWidget(
store: state.storeBean,
onItemTap: (goods) {
EasyThrottle.throttle(
'buy',
Duration(seconds: 3),
() => logic.buyGoods(goods),
);
},
);
},
),
],
),
),
),
),
],
),
);
}
}

View File

@ -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<KtMinePage> {
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<KtMinePage> {
);
}
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<KtMinePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
handleItem(
'ic_order_record.png',
'Order Record',
() => Get.to(
() => KtOrderRecordPage(
),
),
), handleItem(
'ic_privacy.png',
'Privacy Policy',
() => Get.to(

View File

@ -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';

View File

@ -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()),

View File

@ -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<VideoPlayerController?> 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<VideoPlayerController?>.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<String, dynamic> 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<void> 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<KtMineLogic>(
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<KtStoreLogic>(
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,
),
);
},
);
},
),
],
),
),
),
);
}
}

View File

@ -5,6 +5,7 @@ class VideoPlayState {
String imageUrl = '';
int shortPlayId = -1;
num videoId = -1;
int curUnlock = 999;
int? activityId;
bool isFromDiscover = false;
VideoDetailBean? video;

View File

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

View File

@ -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<ProductDetailsResponse> queryProducts(Set<String> productIds) async {
return await _iap.queryProductDetails(productIds);
}
/// /
static Future<void> 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<List<PurchaseDetails>> get purchaseStream => _iap.purchaseStream;
///
static Future<void> 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<void> consumeIfNeeded(PurchaseDetails purchaseDetails) async {
if (purchaseDetails is GooglePlayPurchaseDetails) {
final InAppPurchaseAndroidPlatformAddition addition =
InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition;
await addition.consumePurchase(purchaseDetails);
}
}
///
static Future<bool> isAvailable() async {
return await _iap.isAvailable();
}
///
static Future<void> 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<void> 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<List<PurchaseDetails>> getPendingPurchases() async {
final available = await isAvailable();
if (!available) return [];
final List<PurchaseDetails> pending = [];
//
final completer = Completer<List<PurchaseDetails>>();
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;
}
}

View File

@ -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<void> cacheFailedGoods(KtGoodsBean goods) async {
final prefs = await SharedPreferences.getInstance();
List<String> goodsList = prefs.getStringList(_cacheKey) ?? [];
goodsList.add(json.encode(goods.toJson()));
print('-----goods.toJson:${goods.toJson()}');
await prefs.setStringList(_cacheKey, goodsList);
}
/// goods列表
Future<List<KtGoodsBean>> getCachedGoodsList() async {
final prefs = await SharedPreferences.getInstance();
List<String> goodsList = prefs.getStringList(_cacheKey) ?? [];
return goodsList.map((e) => KtGoodsBean.fromJson(json.decode(e))).toList();
}
///
Future<void> clearCachedGoods() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_cacheKey);
}
/// goods
Future<void> removeGoods(KtGoodsBean goods) async {
final prefs = await SharedPreferences.getInstance();
List<String> goodsList = prefs.getStringList(_cacheKey) ?? [];
goodsList.removeWhere((e) {
final g = KtGoodsBean.fromJson(json.decode(e));
return g.id == goods.id;
});
await prefs.setStringList(_cacheKey, goodsList);
}
}

View File

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

View File

@ -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<KtStoreWidget> createState() => _KtStoreWidgetState();
}
class _KtStoreWidgetState extends State<KtStoreWidget> {
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 <Restore> 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<Widget> initWidget() {
// if (widget.onlyCoins) return [coinList()];
List<Widget> 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<KtGoodsBean> get smallList =>
widget.store.listCoins!.where((item) => item.size == 'small').toList();
List<KtGoodsBean> 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,
),
],
);
}
}

View File

@ -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();