DoggyZhang 3 месяцев назад
Родитель
Сommit
ddd1122cf7
32 измененных файлов с 748 добавлено и 1306 удалено
  1. 6 6
      app/dependencies/releaseRuntimeClasspath.txt
  2. 7 0
      app/src/main/java/com/adealink/weparty/module/wallet/IWalletService.kt
  3. 22 0
      app/src/main/java/com/adealink/weparty/module/wallet/WalletModule.kt
  4. 11 6
      app/src/main/java/com/adealink/weparty/ui/MainStartUpFragment.kt
  5. 1 1
      app/src/main/java/com/adealink/weparty/webview/loader/IResourceLoader.kt
  6. 2 2
      gradle/libs.versions.toml
  7. 2 1
      module/im/src/main/java/com/adealink/weparty/im/list/viewmodel/SessionListViewModel.kt
  8. 54 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/WalletServiceImpl.kt
  9. 6 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/datasource/remote/WalletHttpService.kt
  10. 0 618
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/GoogleBilling.kt
  11. 81 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/GooglePayManager.kt
  12. 263 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/GooglePayUtil.kt
  13. 0 17
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/IPay.kt
  14. 0 8
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/IPayListener.kt
  15. 0 20
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/IPayManager.kt
  16. 0 28
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/PayConstants.kt
  17. 0 474
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/PayManager.kt
  18. 41 0
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/WeakHandler.kt
  19. 0 38
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/model/GooglePurchase.kt
  20. 0 44
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/model/GoogleSkuInfo.kt
  21. 0 5
      module/wallet/src/main/java/com/adealink/weparty/wallet/pay/model/PurchaseResult.kt
  22. 1 1
      module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/adapter/RechargeItemViewBinder.kt
  23. 28 5
      module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/data/WalletData.kt
  24. 5 2
      module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/fragment/CoinRechargeFragment.kt
  25. 2 2
      module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/fragment/DiamondRechargeFragment.kt
  26. 7 7
      module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/viewmodel/RechargeViewModel.kt
  27. 189 4
      module/wallet/src/main/java/com/adealink/weparty/wallet/viewmodel/WalletViewModel.kt
  28. 5 5
      module/wallet/src/main/res/layout/fragment_recharge_coin.xml
  29. 3 3
      module/wallet/src/main/res/layout/fragment_recharge_diamond.xml
  30. 4 3
      module/wallet/src/main/res/values-in/strings.xml
  31. 4 3
      module/wallet/src/main/res/values-zh/strings.xml
  32. 4 3
      module/wallet/src/main/res/values/strings.xml

+ 6 - 6
app/dependencies/releaseRuntimeClasspath.txt

@@ -277,14 +277,14 @@ io.reactivex.rxjava3:rxjava:3.0.4
 javax.inject:javax.inject:1
 org.checkerframework:checker-qual:3.43.0
 org.conscrypt:conscrypt-android:2.5.3
-org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.0
+org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.1.0
 org.jetbrains.kotlin:kotlin-annotations-jvm:1.3.72
 org.jetbrains.kotlin:kotlin-bom:1.8.22
-org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.0
-org.jetbrains.kotlin:kotlin-stdlib-common:2.0.0
-org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
-org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0
-org.jetbrains.kotlin:kotlin-stdlib:2.0.0
+org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0
+org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0
+org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10
+org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10
+org.jetbrains.kotlin:kotlin-stdlib:2.1.0
 org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
 org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
 org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3

+ 7 - 0
app/src/main/java/com/adealink/weparty/module/wallet/IWalletService.kt

@@ -1,11 +1,18 @@
 package com.adealink.weparty.module.wallet
 
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.weparty.aab.IService
 import com.adealink.weparty.module.wallet.viewmodel.IWalletViewModel
 
 interface IWalletService : IService<IWalletService> {
 
+    fun register(lifecycleOwner: LifecycleOwner)
+
+    fun fetchCurrency()
+
+    fun queryPurchaseAsync()
+
     fun getWalletViewModel(owner: ViewModelStoreOwner): IWalletViewModel?
 
 }

+ 22 - 0
app/src/main/java/com/adealink/weparty/module/wallet/WalletModule.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.module.wallet
 
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.BaseDynamicModule
 import com.adealink.weparty.R
@@ -25,12 +26,33 @@ object WalletModule : BaseDynamicModule<IWalletService>(IWalletService::class),
 
             }
 
+            override fun register(lifecycleOwner: LifecycleOwner) {
+            }
+
+            override fun fetchCurrency() {
+            }
+
+            override fun queryPurchaseAsync() {
+            }
+
             override fun getWalletViewModel(owner: ViewModelStoreOwner): IWalletViewModel? {
                 return null
             }
         }
     }
 
+    override fun register(lifecycleOwner: LifecycleOwner) {
+        getService().register(lifecycleOwner)
+    }
+
+    override fun fetchCurrency() {
+        getService().fetchCurrency()
+    }
+
+    override fun queryPurchaseAsync() {
+        getService().queryPurchaseAsync()
+    }
+
 
     override fun getWalletViewModel(owner: ViewModelStoreOwner): IWalletViewModel? {
         return getService().getWalletViewModel(owner)

+ 11 - 6
app/src/main/java/com/adealink/weparty/ui/MainStartUpFragment.kt

@@ -18,6 +18,7 @@ import com.adealink.weparty.config.globalConfigManager
 import com.adealink.weparty.location.viewmodel.LocationReporter
 import com.adealink.weparty.module.account.AccountModule
 import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.module.wallet.WalletModule
 import com.adealink.weparty.push.PushStatEvent
 import com.google.firebase.crashlytics.ktx.crashlytics
 import com.google.firebase.crashlytics.ktx.setCustomKeys
@@ -64,6 +65,9 @@ class MainStartUpFragment : BaseFragment() {
         withContext(Dispatcher.WENEXT_THREAD_POOL) {
             importantLoad()
         }
+        withContext(Dispatcher.UI) {
+            importLoadInUIThread()
+        }
     }
 
     private fun importantLoad() {
@@ -73,7 +77,6 @@ class MainStartUpFragment : BaseFragment() {
             AccountModule.checkRemoteVirtualAppConfig()
 //            AttributionModule.reportAttributionData()
             BaseStatEvent.setUserId(ProfileModule.getMyUid().safeToLong())
-//            ShareModule.reportUserType()
             pushService.getPushTokenAndReport()
             Firebase.crashlytics.setCustomKeys {
                 key("uid", AccountModule.uid)
@@ -87,13 +90,15 @@ class MainStartUpFragment : BaseFragment() {
         AccountModule.refreshToken()
 //        ProfileModule.reportPhoneModel(FromScene.STARTUP.scene)
 //        MessageModule.activityOnCreateMainTask()
-//        RoomModule.init()
-//        GameModule.checkPlayingGame()
-
         installDynamicModule()
         Log.d(TAG, "importantLoad-end, cost:${SystemClock.elapsedRealtime() - startTs}ms")
     }
 
+
+    private fun importLoadInUIThread() {
+        WalletModule.register(this)
+    }
+
     // 启动后,交给Google Play,延迟安装所有模块(后台自动下载)
     private fun installDynamicModule() {
         AAB.deferredInstall(
@@ -112,8 +117,8 @@ class MainStartUpFragment : BaseFragment() {
     private fun minorLoad() {
         val startTs = SystemClock.elapsedRealtime()
         Log.d(TAG, "minorLoad-start")
-//        WalletModule.fetchCurrency()
-//        WalletModule.queryAndHandleUnDealPurchases()
+        WalletModule.fetchCurrency()
+        WalletModule.queryPurchaseAsync()
 //        LevelModule.pullLevelConfigs()
 //        CoupleModule.pullCoupleConfig()
 //        WalletModule.init()

+ 1 - 1
app/src/main/java/com/adealink/weparty/webview/loader/IResourceLoader.kt

@@ -61,7 +61,7 @@ interface IResourceLoader {
     fun filterHeaders(map: Map<String, String>): Map<String, String> {
         val filterHeaders = hashMapOf<String, String>()
         map.entries.forEach {
-            val lowerKey = it.key.toLowerCase(Locale.ROOT)
+            val lowerKey = it.key.lowercase(Locale.ROOT)
             if (REPORT_HEADERS.contains(lowerKey)) {
                 filterHeaders[lowerKey.replace("-", "_")] = it.value
             }

+ 2 - 2
gradle/libs.versions.toml

@@ -12,7 +12,7 @@ kapt = "2.0.20-Beta2"
 desugar_jdk_libs = "2.1.5"
 
 #kotin
-kotlin = "1.9.0"
+kotlin = "2.1.0"
 kotlinxCoroutine = "1.7.3"
 
 # test
@@ -70,7 +70,7 @@ facebook = "17.0.0"
 whatsappOtpAndroidSdk = "0.1.0"
 
 # billing
-googleBilling = "7.0.0"
+googleBilling = "8.0.0"
 payermax = "1.0.08"
 
 # rtc

+ 2 - 1
module/im/src/main/java/com/adealink/weparty/im/list/viewmodel/SessionListViewModel.kt

@@ -49,7 +49,8 @@ class SessionListViewModel : BaseViewModel() {
     }
 
     private suspend fun queryUserOnline(dataSource: List<ConversationInfo>): Map<String, UserInfo>{
-
+        // TODO: zhangfei
+        return emptyMap()
     }
 
 }

+ 54 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/WalletServiceImpl.kt

@@ -1,12 +1,23 @@
 package com.adealink.weparty.wallet
 
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStoreOwner
+import com.adealink.frame.log.Log
 import com.adealink.frame.spi.RegisterService
+import com.adealink.frame.util.AppUtil
 import com.adealink.weparty.module.wallet.IWalletService
 import com.adealink.weparty.module.wallet.viewmodel.IWalletViewModel
+import com.adealink.weparty.wallet.data.TAG_WALLET
+import com.adealink.weparty.wallet.manager.walletManager
+import com.adealink.weparty.wallet.pay.GooglePayManager
+import com.adealink.weparty.wallet.pay.GooglePayUtil
 import com.adealink.weparty.wallet.viewmodel.WalletViewModel
 import com.adealink.weparty.wallet.viewmodel.WalletViewModelFactory
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.Purchase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
 @RegisterService(IWalletService::class)
 class WalletServiceImpl : IWalletService {
@@ -19,6 +30,49 @@ class WalletServiceImpl : IWalletService {
         return this
     }
 
+    override fun register(lifecycleOwner: LifecycleOwner) {
+        GooglePayManager.init(AppUtil.appContext)
+//        GooglePayManager.addListener(object : GooglePayUtil.OnPurchasesListener {
+//            override fun onQueryPurchasesResponse(
+//                result: BillingResult, purchases: MutableList<Purchase>
+//            ) {
+//                purchases.filter { it.purchaseState == 1 }.forEach {
+//                    Log.e(TAG_WALLET, "product:${it.originalJson}\nIdentifiers:${mGson.toJson(it.accountIdentifiers)}")
+//                    if (it.accountIdentifiers?.obfuscatedProfileId?.uppercase()?.startsWith("GIFTPACK_") != true) {
+//                        mGooglePayNotifyList.add(
+//                            GooglePayMissBean(
+//                                true, it.accountIdentifiers?.obfuscatedProfileId ?: "", it.accountIdentifiers?.obfuscatedAccountId ?: "", it.purchaseToken
+//                            )
+//                        )
+//                    }
+//
+//                    if (it.accountIdentifiers?.obfuscatedProfileId?.uppercase()?.startsWith("GIFTPACK_") == true) {
+//                        mViewModel.rechargeSuccessReport(
+//                            mutableMapOf(
+//                                Pair("userId", MyApplication.mUserInfo?.profile?.id ?: ""), Pair("orderId", it.accountIdentifiers?.obfuscatedProfileId ?: ""), Pair("receipt", it.purchaseToken)
+//                            )
+//                        )
+//                    }
+//                }
+//                notifyGooglePayResult()
+//            }
+//
+//            override fun onSetupFinish() {
+//                launch(Dispatchers.IO) {
+//                    GooglePayManager.getGooglePayClient()?.queryPurchaseAsync(GooglePayUtil.TYPE_IN_APP)
+//                }
+//            }
+//        })
+    }
+
+    override fun fetchCurrency() {
+        walletManager.refreshWalletData()
+    }
+
+    override fun queryPurchaseAsync() {
+
+    }
+
     override fun getWalletViewModel(owner: ViewModelStoreOwner): IWalletViewModel {
         return ViewModelProvider(owner, WalletViewModelFactory())[WalletViewModel::class.java]
     }

+ 6 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/datasource/remote/WalletHttpService.kt

@@ -6,6 +6,8 @@ import com.adealink.weparty.wallet.data.WalletRes
 import com.adealink.weparty.wallet.detail.data.DetailReq
 import com.adealink.weparty.wallet.detail.data.DetailRes
 import com.adealink.weparty.wallet.recharge.data.ConvertCurrencyReq
+import com.adealink.weparty.wallet.recharge.data.PrePurchaseReq
+import com.adealink.weparty.wallet.recharge.data.PrePurchaseRes
 import com.adealink.weparty.wallet.recharge.data.RechargeConfigReq
 import com.adealink.weparty.wallet.recharge.data.RechargeConfigRes
 import retrofit2.http.Body
@@ -27,4 +29,8 @@ interface WalletHttpService {
 
     @POST("wallet/exchange")
     suspend fun convertCurrency(@Body req: ConvertCurrencyReq): Rlt<Res<Any>>
+
+    @POST("wallet/recharge/submit")
+    suspend fun prePurchase(@Body req: PrePurchaseReq): Rlt<Res<PrePurchaseRes>>
+
 }

+ 0 - 618
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/GoogleBilling.kt

@@ -1,618 +0,0 @@
-package com.adealink.weparty.wallet.pay
-
-import android.app.Activity
-import com.adealink.frame.base.IError
-import com.adealink.frame.base.Rlt
-import com.adealink.frame.coroutine.dispatcher.Dispatcher
-import com.adealink.frame.frame.BaseFrame
-import com.adealink.frame.log.Log
-import com.adealink.frame.util.AppUtil
-import com.adealink.weparty.module.account.AccountModule
-import com.adealink.weparty.module.wallet.data.PayChannel
-import com.adealink.weparty.module.wallet.data.PayChannelSetupError
-import com.adealink.weparty.module.wallet.data.PurchaseInfo
-import com.adealink.weparty.module.wallet.data.SkuInfo
-import com.adealink.weparty.module.wallet.data.SkuQueryError
-import com.adealink.weparty.wallet.data.TAG_GOOGLE_BILLING
-import com.adealink.weparty.wallet.pay.model.GooglePurchase
-import com.adealink.weparty.wallet.pay.model.GoogleSkuInfo
-import com.adealink.weparty.wallet.pay.model.PurchaseResult
-import com.android.billingclient.api.BillingClient
-import com.android.billingclient.api.BillingClientStateListener
-import com.android.billingclient.api.BillingFlowParams
-import com.android.billingclient.api.BillingResult
-import com.android.billingclient.api.ConsumeParams
-import com.android.billingclient.api.PurchasesUpdatedListener
-import com.android.billingclient.api.QueryProductDetailsParams
-import com.android.billingclient.api.QueryPurchasesParams
-import com.android.billingclient.api.SkuDetailsParams
-import com.android.billingclient.api.consumePurchase
-import com.android.billingclient.api.queryProductDetails
-import com.android.billingclient.api.queryPurchasesAsync
-import com.android.billingclient.api.querySkuDetails
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.math.pow
-
-
-class GoogleBilling : BaseFrame<IPayListener>(), IPay {
-
-    /**
-     * https://developer.android.com/reference/com/android/billingclient/api/BillingClient
-     * 调试BillingClient对象: adb shell setprop log.tag.BillingClient VERBOSE
-     */
-    private var billingClient: BillingClient? = null
-
-    private val _purchaseUpdates = MutableSharedFlow<PurchaseResult>(
-        replay = 0,                         // 不要缓存旧值
-        extraBufferCapacity = 1,            // 防止 emit 太快被丢弃
-        onBufferOverflow = BufferOverflow.DROP_OLDEST
-    )
-    private val purchaseUpdates: SharedFlow<PurchaseResult> = _purchaseUpdates
-    private var purchasingSkuIdSet: MutableSet<String> = mutableSetOf()
-
-    private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
-        Log.i(
-            TAG_GOOGLE_BILLING,
-            "purchaseUpdate, resultCode: ${billingResult.responseCode}, skuIds:${purchases?.map { it.products.getOrNull(0) ?: "" }},debugMessage:${billingResult.debugMessage}"
-        )
-        val resCode = getPayResCodeFromBillingResponse(billingResult.responseCode)
-        val googlePurchaseInfo =
-            PurchaseInfo(
-                PayChannel.Google,
-                purchases?.map { GooglePurchase(it) } ?: arrayListOf<GooglePurchase>()
-            )
-        launch {
-            if (resCode == PayRes.SUCCESS) {
-                //先处理延迟成功交易
-                val pendingPurchase =
-                    googlePurchaseInfo.purchases.filter { !purchasingSkuIdSet.contains(it.getSkuId()) }
-                val pendingPurchaseInfo = PurchaseInfo(
-                    PayChannel.Google,
-                    pendingPurchase
-                )
-                dispatch {
-                    it.onPendingPurchaseResult(
-                        PurchaseResult(
-                            resCode,
-                            pendingPurchaseInfo
-                        )
-                    )
-                }
-            }
-            _purchaseUpdates.emit(
-                PurchaseResult(
-                    resCode,
-                    googlePurchaseInfo
-                )
-            )
-        }
-    }
-
-    init {
-        billingClient = BillingClient.newBuilder(AppUtil.appContext)
-            .enablePendingPurchases()
-            .setListener(purchasesUpdatedListener)
-            .build()
-    }
-
-    private suspend fun ensureConnection(): BillingClient? {
-        // 如果已经连接,直接返回
-        billingClient?.takeIf { it.isReady }?.let { return it }
-
-        // 否则建立新连接
-        val rlt = setup()
-        return when (rlt) {
-            is Rlt.Failed -> {
-                null
-            }
-
-            is Rlt.Success -> {
-                billingClient
-            }
-        }
-    }
-
-    private suspend fun BillingClient.startConnection(): BillingResult = suspendCancellableCoroutine { continuation ->
-        val alreadyResume = AtomicBoolean(false)
-        startConnection(object : BillingClientStateListener {
-            override fun onBillingSetupFinished(billingResult: BillingResult) {
-                Log.i(
-                    TAG_GOOGLE_BILLING,
-                    "onBillingSetupFinished, resultCode: ${billingResult.responseCode}, debugMessage:${billingResult.debugMessage}"
-                )
-                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
-                    if (alreadyResume.compareAndSet(false, true) && continuation.isActive) {
-                        Log.i(TAG_GOOGLE_BILLING, "Billing setup finished successfully.")
-                        continuation.resume(billingResult)
-                    }
-                } else {
-                    if (alreadyResume.compareAndSet(false, true) && continuation.isActive) {
-                        Log.d(TAG_GOOGLE_BILLING, "Billing setup finished fail.")
-                        continuation.resumeWithException(BillingConnectionException(billingResult))
-                    }
-                }
-            }
-
-            /**
-             * https://developer.android.com/reference/com/android/billingclient/api/BillingClientStateListener#onBillingServiceDisconnected()
-             *
-             * Note: This does not remove the billing service connection itself - this binding to the service will remain active,
-             * and you will receive a call to onBillingSetupFinished when the billing service is next running and setup is complete.
-             */
-            override fun onBillingServiceDisconnected() {
-                // 这里不调用 continuation,因为这不是连接完成的回调
-                Log.i(TAG_GOOGLE_BILLING, "onBillingServiceDisconnected")
-            }
-        })
-
-        // 如果协程被取消,则结束连接
-        continuation.invokeOnCancellation {
-            endConnection()
-        }
-    }
-
-    class BillingConnectionException(val billingResult: BillingResult) : Exception(billingResult.debugMessage)
-
-    override suspend fun setup(): Rlt<Any> {
-        return withContext(this.coroutineContext) {
-            if (billingClient?.isReady == true) {
-                return@withContext Rlt.Success(Any())
-            }
-
-            val maxRetries = 3
-            var attempt = 0
-            val retryDelay = 1000L // 1秒重试间隔
-            var lastException: Exception? = null
-
-            while (attempt < maxRetries) {
-                try {
-                    val client = BillingClient.newBuilder(AppUtil.appContext)
-                        .enablePendingPurchases()
-                        .setListener(purchasesUpdatedListener)
-                        .build()
-
-                    val result = client.startConnection()
-                    if (result.responseCode == BillingClient.BillingResponseCode.OK) {
-                        billingClient = client
-                        return@withContext Rlt.Success(Any())
-                    } else {
-                        throw BillingConnectionException(result)
-                    }
-                } catch (e: Exception) {
-                    lastException = e
-                    attempt++
-                    if (attempt < maxRetries) {
-                        delay(retryDelay)
-                    }
-                }
-            }
-
-            return@withContext if (lastException is BillingConnectionException) {
-                Rlt.Failed(
-                    PayChannelSetupError(
-                        code = getPayResCodeFromBillingResponse(lastException.billingResult.responseCode)
-                    )
-                )
-            } else {
-                Rlt.Failed(
-                    PayChannelSetupError(
-                        code = -1
-                    )
-                )
-            }
-        }
-    }
-
-    override suspend fun querySkuDetails(skuIdList: List<String>): Rlt<List<SkuInfo>> {
-        return withContext(this.coroutineContext) {
-            if (isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS)) {
-                queryProductDetailsRetry(skuIdList)
-            } else {
-                querySkuDetailsRetry(skuIdList)
-            }
-        }
-    }
-
-    private suspend fun querySkuDetailsRetry(skuIdList: List<String>): Rlt<List<SkuInfo>> {
-        val params = SkuDetailsParams.newBuilder()
-            .setSkusList(skuIdList)
-            .setType(BillingClient.SkuType.INAPP)
-            .build()
-
-        // 最大重试次数
-        val maxRetries = 3
-        // 初始延迟时间(毫秒)
-        val initialDelay = 1000L
-        // 延迟倍数,用于指数回退
-        val delayFactor = 2.0
-        var attempt = 0
-
-        while (true) {
-            val client = ensureConnection()
-            if (client == null || client.isReady.not()) {
-                return Rlt.Failed(SkuQueryError(PayRes.SET_UP_DISCONNECTED))
-            }
-            val skuDetailsResult = withContext(Dispatcher.WENEXT_THREAD_POOL) {
-                client.querySkuDetails(params)
-            }
-            Log.i(
-                TAG_GOOGLE_BILLING,
-                "querySkuDetails, resultCode: ${skuDetailsResult.billingResult.responseCode}, " +
-                        "debugMessage:${skuDetailsResult.billingResult.debugMessage}, " +
-                        "skuDetailSize: ${skuDetailsResult.skuDetailsList?.size}"
-            )
-
-            when (skuDetailsResult.billingResult.responseCode) {
-                BillingClient.BillingResponseCode.OK -> {
-                    if (skuDetailsResult.skuDetailsList.isNullOrEmpty()) {
-                        return Rlt.Failed(SkuQueryError(PayRes.PURCHASE_SKU_UN_MATCH))
-                    }
-                    return Rlt.Success(skuDetailsResult.skuDetailsList!!.map {
-                        GoogleSkuInfo(skuDetailsOld = it)
-                    })
-                }
-
-                in setOf(
-                    BillingClient.BillingResponseCode.ERROR,
-                    BillingClient.BillingResponseCode.NETWORK_ERROR,
-                    BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
-                    BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
-                ) -> {
-                    attempt++
-                    if (attempt >= maxRetries) {
-                        return Rlt.Failed(
-                            SkuQueryError(
-                                getPayResCodeFromBillingResponse(
-                                    skuDetailsResult.billingResult.responseCode
-                                )
-                            )
-                        )
-                    }
-                    delay(initialDelay * delayFactor.pow(attempt.toDouble()).toLong())
-                }
-
-                else -> {
-                    return Rlt.Failed(
-                        SkuQueryError(
-                            getPayResCodeFromBillingResponse(
-                                skuDetailsResult.billingResult.responseCode
-                            )
-                        )
-                    )
-                }
-            }
-        }
-    }
-
-    private suspend fun queryProductDetailsRetry(skuIdList: List<String>): Rlt<List<SkuInfo>> {
-        val productList = skuIdList.map {
-            QueryProductDetailsParams.Product.newBuilder()
-                .setProductId(it)
-                .setProductType(BillingClient.ProductType.INAPP)
-                .build()
-        }
-        val params = QueryProductDetailsParams.newBuilder()
-            .setProductList(productList)
-            .build()
-
-        // 最大重试次数
-        val maxRetries = 3
-        // 初始延迟时间(毫秒)
-        val initialDelay = 1000L
-        // 延迟倍数,用于指数回退
-        val delayFactor = 2.0
-        var attempt = 0
-
-        while (true) {
-            val client = ensureConnection()
-            if (client == null || client.isReady.not()) {
-                return Rlt.Failed(SkuQueryError(PayRes.SET_UP_DISCONNECTED))
-            }
-            val productDetailsResult = withContext(Dispatcher.WENEXT_THREAD_POOL) {
-                client.queryProductDetails(params)
-            }
-            Log.i(
-                TAG_GOOGLE_BILLING,
-                "queryProductDetails, resultCode: ${productDetailsResult.billingResult.responseCode}, " +
-                        "debugMessage:${productDetailsResult.billingResult.debugMessage}, " +
-                        "skuDetailSize: ${productDetailsResult.productDetailsList?.size}"
-            )
-
-            when (productDetailsResult.billingResult.responseCode) {
-                BillingClient.BillingResponseCode.OK -> {
-                    if (productDetailsResult.productDetailsList.isNullOrEmpty()) {
-                        return Rlt.Failed(SkuQueryError(PayRes.PURCHASE_SKU_UN_MATCH))
-                    }
-                    return Rlt.Success(productDetailsResult.productDetailsList!!.map {
-                        GoogleSkuInfo(
-                            skuDetails = it
-                        )
-                    })
-                }
-
-                in setOf(
-                    BillingClient.BillingResponseCode.ERROR,
-                    BillingClient.BillingResponseCode.NETWORK_ERROR,
-                    BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
-                    BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
-                ) -> {
-                    attempt++
-                    if (attempt >= maxRetries) {
-                        return Rlt.Failed(
-                            SkuQueryError(getPayResCodeFromBillingResponse(productDetailsResult.billingResult.responseCode))
-                        )
-                    }
-                    delay(initialDelay * delayFactor.pow(attempt.toDouble()).toLong())
-                }
-
-                else -> {
-                    return Rlt.Failed(
-                        SkuQueryError(getPayResCodeFromBillingResponse(productDetailsResult.billingResult.responseCode))
-                    )
-                }
-            }
-        }
-    }
-
-    override suspend fun consume(token: String): Rlt<Any?> {
-        return withContext(this.coroutineContext) {
-            consumeRetry(token)
-        }
-    }
-
-    private suspend fun consumeRetry(token: String): Rlt<Any?> {
-        // 最大重试次数
-        val maxRetries = 3
-        // 初始延迟时间(毫秒)
-        val initialDelay = 1000L
-        // 延迟倍数,用于指数回退
-        val delayFactor = 2.0
-        var attempt = 0
-
-        val params = ConsumeParams.newBuilder()
-            .setPurchaseToken(token)
-            .build()
-        while (true) {
-            val client = ensureConnection()
-            if (client == null || client.isReady.not()) {
-                return Rlt.Failed(SkuQueryError(PayRes.SET_UP_DISCONNECTED))
-            }
-
-            val consumeResult = client.consumePurchase(params)
-
-            val responseCode = consumeResult.billingResult.responseCode
-            Log.i(
-                TAG_GOOGLE_BILLING,
-                "consumePurchase, resultCode: $responseCode, "
-                        + "debugMessage:${consumeResult.billingResult.debugMessage}"
-            )
-
-            when (responseCode) {
-                BillingClient.BillingResponseCode.OK -> {
-                    return Rlt.Success(null)
-                }
-
-                in setOf(
-                    BillingClient.BillingResponseCode.ERROR,
-                    BillingClient.BillingResponseCode.NETWORK_ERROR,
-                    BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
-                    BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
-                ) -> {
-                    attempt++
-                    if (attempt >= maxRetries) {
-                        return Rlt.Failed(
-                            IError(serverCode = getPayResCodeFromBillingResponse(responseCode))
-                        )
-                    }
-                    delay(initialDelay * delayFactor.pow(attempt.toDouble()).toLong())
-                }
-
-                else -> {
-                    return Rlt.Failed(
-                        IError(serverCode = getPayResCodeFromBillingResponse(responseCode))
-                    )
-                }
-            }
-        }
-    }
-
-    override suspend fun launchPaymentFlow(
-        activity: Activity,
-        skuInfo: SkuInfo,
-        orderId: String
-    ): PurchaseResult {
-        return withContext(this.coroutineContext) {
-            launchPaymentFlowRetry(activity, skuInfo, orderId)
-        }
-    }
-
-    private suspend fun launchPaymentFlowRetry(
-        activity: Activity,
-        skuInfo: SkuInfo,
-        orderId: String
-    ): PurchaseResult {
-        // 最大重试次数
-        val maxRetries = 3
-        // 初始延迟时间(毫秒)
-        val initialDelay = 1000L
-        // 延迟倍数,用于指数回退
-        val delayFactor = 2.0
-        var attempt = 0
-
-        val params = BillingFlowParams.newBuilder().apply {
-            if (isFeatureSupported(BillingClient.FeatureType.PRODUCT_DETAILS)) {
-                val skuDetails = (skuInfo as? GoogleSkuInfo)?.skuDetails
-                    ?: return PurchaseResult(
-                        resCode = PayRes.PURCHASE_SKU_UN_MATCH,
-                        purchaseInfo = null
-                    )
-                setProductDetailsParamsList(
-                    listOf(
-                        BillingFlowParams.ProductDetailsParams.newBuilder()
-                            .setProductDetails(skuDetails)
-                            .build()
-                    )
-                )
-            } else {
-                val skuDetails = (skuInfo as? GoogleSkuInfo)?.skuDetailsOld
-                    ?: return PurchaseResult(
-                        resCode = PayRes.PURCHASE_SKU_UN_MATCH,
-                        purchaseInfo = null
-                    )
-                setSkuDetails(skuDetails)
-            }
-            setObfuscatedAccountId(AccountModule.uid.toString())
-            setObfuscatedProfileId(orderId)
-        }.build()
-
-        while (true) {
-            Log.i(TAG_GOOGLE_BILLING, "launchBillingFlow: skuId = ${skuInfo.getSkuId()}")
-            val client = ensureConnection()
-            if (client == null || client.isReady.not()) {
-                return PurchaseResult(PayRes.SET_UP_DISCONNECTED, null)
-            }
-            purchasingSkuIdSet.add(skuInfo.getSkuId())
-            val launchResult = withContext(Dispatcher.UI) {
-                client.launchBillingFlow(activity, params)
-            }
-
-            val purchaseResult = when {
-                launchResult.responseCode != BillingClient.BillingResponseCode.OK -> {
-                    //发起支付流程不成功,可以直接重试或返回
-                    PurchaseResult(
-                        getPayResCodeFromBillingResponse(launchResult.responseCode),
-                        null
-                    )
-                }
-
-                else -> {
-                    //等待当前支付结果
-                    purchaseUpdates
-                        .first { result ->
-                            result.purchaseInfo?.purchases?.any { it.getSkuId() == skuInfo.getSkuId() } == true
-                                    || result.resCode != BillingClient.BillingResponseCode.OK
-                        }
-                }
-            }
-
-            when (purchaseResult.resCode) {
-                in setOf(
-                    PayRes.ERROR,
-                    PayRes.NETWORK_ERROR,
-                    PayRes.SERVICE_DISCONNECTED,
-                    PayRes.SERVICE_UNAVAILABLE
-                ) -> {
-                    attempt++
-                    if (attempt >= maxRetries) {
-                        purchasingSkuIdSet.remove(skuInfo.getSkuId())
-                        return purchaseResult
-                    }
-                    delay(initialDelay * delayFactor.pow(attempt.toDouble()).toLong())
-                }
-
-                else -> {
-                    purchasingSkuIdSet.remove(skuInfo.getSkuId())
-                    return purchaseResult
-                }
-            }
-        }
-    }
-
-    override suspend fun queryPurchases(): Rlt<PurchaseInfo> {
-        return withContext(this.coroutineContext) {
-            queryPurchasesRetry()
-        }
-    }
-
-    private suspend fun queryPurchasesRetry(): Rlt<PurchaseInfo> {
-        // 最大重试次数
-        val maxRetries = 3
-        // 初始延迟时间(毫秒)
-        val initialDelay = 1000L
-        // 延迟倍数,用于指数回退
-        val delayFactor = 2.0
-        var attempt = 0
-
-        while (true) {
-            val client = ensureConnection()
-            if (client == null || client.isReady.not()) {
-                return Rlt.Failed(SkuQueryError(PayRes.SET_UP_DISCONNECTED))
-            }
-            val result = client.queryPurchasesAsync(
-                QueryPurchasesParams.newBuilder()
-                    .setProductType(BillingClient.ProductType.INAPP)
-                    .build()
-            )
-            val responseCode = result.billingResult.responseCode
-            Log.i(TAG_GOOGLE_BILLING, "queryPurchases, resultCode: $responseCode")
-
-            when (responseCode) {
-                BillingClient.BillingResponseCode.OK -> {
-                    val googlePurchaseInfo = PurchaseInfo(
-                        PayChannel.Google,
-                        result.purchasesList.map { GooglePurchase(it) }
-                    )
-                    return Rlt.Success(googlePurchaseInfo)
-                }
-
-                in setOf(
-                    BillingClient.BillingResponseCode.ERROR,
-                    BillingClient.BillingResponseCode.NETWORK_ERROR,
-                    BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
-                    BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
-                ) -> {
-                    attempt++
-                    if (attempt >= maxRetries) {
-                        return Rlt.Failed(
-                            IError(serverCode = getPayResCodeFromBillingResponse(responseCode))
-                        )
-                    }
-                    delay(initialDelay * delayFactor.pow(attempt.toDouble()).toLong())
-                }
-
-                else -> return Rlt.Failed(
-                    IError(serverCode = getPayResCodeFromBillingResponse(responseCode))
-                )
-            }
-        }
-    }
-
-    private suspend fun isFeatureSupported(feature: String): Boolean {
-        return withContext(this.coroutineContext) {
-            val client = ensureConnection()
-            if (client == null || client.isReady.not()) {
-                false
-            } else {
-                client.isFeatureSupported(feature).responseCode == BillingClient.BillingResponseCode.OK
-            }
-        }
-    }
-
-    private fun getPayResCodeFromBillingResponse(responseCode: Int): Int {
-        return when (responseCode) {
-            BillingClient.BillingResponseCode.OK -> PayRes.SUCCESS
-            BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> PayRes.FEATURE_NOT_SUPPORTED
-            BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> PayRes.SERVICE_DISCONNECTED
-            BillingClient.BillingResponseCode.USER_CANCELED -> PayRes.PURCHASE_USER_CANCEL
-            BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> PayRes.SERVICE_UNAVAILABLE
-            BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> PayRes.BILLING_UNAVAILABLE
-            BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> PayRes.ITEM_UNAVAILABLE
-            BillingClient.BillingResponseCode.DEVELOPER_ERROR -> PayRes.DEVELOPER_ERROR
-            BillingClient.BillingResponseCode.ERROR -> PayRes.ERROR
-            BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> PayRes.ITEM_ALREADY_OWNED
-            BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> PayRes.ITEM_NOT_OWNED
-            BillingClient.BillingResponseCode.NETWORK_ERROR -> PayRes.NETWORK_ERROR
-            else -> responseCode
-        }
-    }
-}

+ 81 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/GooglePayManager.kt

@@ -0,0 +1,81 @@
+package com.adealink.weparty.wallet.pay
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.Log
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.Purchase
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+/**
+ * @author  null
+ * @date    2024/3/13 18:20
+ * @desc
+ *
+ * Everything will be alright in the end, so if it's not alright then it's not the end.
+ */
+@SuppressLint("StaticFieldLeak")
+object GooglePayManager {
+
+    private var mClient: GooglePayUtil? = null
+
+    private val mListeners = mutableListOf<GooglePayUtil.OnPurchasesListener>()
+
+    private val mListener = object : GooglePayUtil.OnPurchasesListener {
+        override fun onQueryPurchasesResponse(result: BillingResult, purchases: MutableList<Purchase>) {
+            mListeners.forEach { it.onQueryPurchasesResponse(result, purchases) }
+        }
+        override fun onPurchasesSuccess(purchases: List<Purchase>) {
+            mListeners.forEach { it.onPurchasesSuccess(purchases) }
+        }
+        override fun onPurchasesCancel(purchases: List<Purchase>?) {
+            mListeners.forEach { it.onPurchasesCancel(purchases) }
+        }
+        override fun onPurchasesFailure(result: BillingResult, purchases: List<Purchase>?) {
+            mListeners.forEach { it.onPurchasesFailure(result, purchases) }
+        }
+        override fun onSetupFinish() {
+            mListeners.forEach { it.onSetupFinish() }
+        }
+    }
+
+    fun init(context: Context) {
+        if (mClient == null) {
+            mClient = GooglePayUtil(context.applicationContext, mListener)
+        }
+    }
+
+    fun addListener(listener: GooglePayUtil.OnPurchasesListener) {
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener)
+        }
+    }
+
+    fun removeListener(listener: GooglePayUtil.OnPurchasesListener) {
+        mListeners.remove(listener)
+    }
+
+    fun getGooglePayClient(): GooglePayUtil? {
+        return mClient
+    }
+
+    @OptIn(DelicateCoroutinesApi::class)
+    fun onResume() {
+        GlobalScope.launch(Dispatchers.Main) {
+            mClient?.queryPurchaseAsync(GooglePayUtil.Companion.TYPE_IN_APP)
+        }
+    }
+
+    fun release() {
+        mClient?.release()
+        mClient = null
+    }
+
+    fun log(msg: String) {
+        Log.e("GooglePayManager", msg)
+    }
+
+}

+ 263 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/GooglePayUtil.kt

@@ -0,0 +1,263 @@
+package com.adealink.weparty.wallet.pay
+
+import android.app.Activity
+import android.content.Context
+import android.os.Handler
+import android.os.Message
+import android.util.Log
+import com.android.billingclient.api.AcknowledgePurchaseParams
+import com.android.billingclient.api.AcknowledgePurchaseResponseListener
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.BillingClient.BillingResponseCode
+import com.android.billingclient.api.BillingClient.ProductType
+import com.android.billingclient.api.BillingClientStateListener
+import com.android.billingclient.api.BillingFlowParams
+import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ConsumeParams
+import com.android.billingclient.api.PendingPurchasesParams
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.ProductDetailsResponseListener
+import com.android.billingclient.api.ProductDetailsResult
+import com.android.billingclient.api.Purchase
+import com.android.billingclient.api.PurchasesResponseListener
+import com.android.billingclient.api.PurchasesUpdatedListener
+import com.android.billingclient.api.QueryProductDetailsParams
+import com.android.billingclient.api.QueryProductDetailsParams.Product
+import com.android.billingclient.api.QueryPurchasesParams
+import com.android.billingclient.api.consumePurchase
+import com.android.billingclient.api.queryProductDetails
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * @author  null
+ * @date    2024/1/10 19:59
+ * @desc
+ *
+ * Everything will be alright in the end, so if it's not alright then it's not the end.
+ */
+class GooglePayUtil(context: Context, private var listener: OnPurchasesListener) {
+
+    companion object {
+        const val TYPE_SUBS = ProductType.SUBS
+        const val TYPE_IN_APP = ProductType.INAPP
+    }
+
+    private var mClient: BillingClient? = null
+    private var mHandler: WeakHandler<GooglePayUtil>? = null
+
+    interface OnPurchasesListener {
+        fun onQueryPurchasesResponse(result: BillingResult, purchases: MutableList<Purchase>) {}
+        fun onPurchasesSuccess(purchases: List<Purchase>) {}
+        fun onPurchasesCancel(purchases: List<Purchase>?) {}
+        fun onPurchasesFailure(result: BillingResult, purchases: List<Purchase>?) {}
+        fun onSetupFinish() {}
+    }
+
+    //购买状态监听器
+    private var mPurchasesUpdatedListener = PurchasesUpdatedListener { result, purchases ->
+        log("PurchasesUpdatedListener result:${result.responseCode}  purchases:$purchases")
+        if (result.responseCode == BillingResponseCode.OK && purchases != null) {
+            listener.onPurchasesSuccess(purchases)
+        } else if (result.responseCode == BillingResponseCode.USER_CANCELED) {
+            listener.onPurchasesCancel(purchases)
+        } else {
+            listener.onPurchasesFailure(result, purchases)
+        }
+    }
+
+    private var mPurchaseResponseListener = PurchasesResponseListener { result, purchases ->
+        log("mPurchaseResponseListener result:${result.responseCode}  p1:$purchases")
+        listener.onQueryPurchasesResponse(result, purchases)
+    }
+
+    //连接状态监听器
+    private var mConnectListener = object : BillingClientStateListener {
+        override fun onBillingServiceDisconnected() {
+            log("onBillingServiceDisconnected")
+            mHandler?.sendEmptyMessageDelayed(0, 1000)
+        }
+
+        override fun onBillingSetupFinished(result: BillingResult) {
+            log("onBillingSetupFinished --> ${result.responseCode}   ${result.debugMessage}")
+            if (result.responseCode == BillingResponseCode.OK) {
+                listener.onSetupFinish()
+            }
+        }
+    }
+
+    //核销状态监听器
+    private var mAcknowledgeListener = AcknowledgePurchaseResponseListener {
+
+    }
+
+    init {
+        mHandler = WeakHandler(this, object : WeakHandler.IOnHandlerCallback<GooglePayUtil> {
+            override fun onMessage(t: GooglePayUtil, msg: Message?, instance: Handler) {
+                mClient?.startConnection(mConnectListener)
+            }
+        })
+        mClient = BillingClient.newBuilder(context.applicationContext)
+            .setListener(mPurchasesUpdatedListener)
+            .enablePendingPurchases(
+                PendingPurchasesParams.newBuilder()
+                    .enablePrepaidPlans()
+                    .enableOneTimeProducts().build()
+            )
+            .build()
+
+        mClient?.startConnection(mConnectListener)
+
+        // 货币相关的信息
+//        val mCurrencyMap = ConcurrentHashMap<String, String>()
+//        Currency.getAvailableCurrencies().toMutableList().forEach {
+//            mCurrencyMap[it.currencyCode] = it.symbol
+//        }
+    }
+
+    /**
+     * Step 1 --> 查询商品详情列表
+     * @param productId     Google Play 管理中心配置的商品 id
+     * @param type  ProductType.SUBS | ProductType.INAPP
+     */
+    fun queryProductDetails(productId: String, type: String, listener: ProductDetailsResponseListener) {
+        log("queryProductDetails productId:$productId   type:$type")
+        val params = QueryProductDetailsParams.newBuilder()
+            .setProductList(
+                arrayListOf(
+                    Product.newBuilder()
+                        .setProductId(productId)
+                        .setProductType(type)
+                        .build()
+                )
+            )
+            .build()
+        mClient?.queryProductDetailsAsync(params, listener)
+    }
+
+    /**
+     * Step 2 --> 购买
+     * @param offerToken    从后端获取的购买token
+     * @param product       从google 获取的商品信息
+     * @param personalized  是否自动化决策进行了个性化设置
+     */
+    fun purchases(activity: Activity, product: ProductDetails, userId: String, selfOrderId: String, offerToken: String? = null, personalized: Boolean? = null) {
+        log("purchases\nproduct:${product.name}\nid:${product.productId}\ntype:${product.productType}")
+        val list = arrayListOf(
+            ProductDetailsParams.newBuilder()
+                .setProductDetails(product)
+                .apply {
+                    if (product.productType == ProductType.SUBS) {
+                        if (offerToken == null) {
+                            throw Exception("purchases error, product -> ${product.productId} is type ProductType.SUBS, offerToken is request!!")
+                        }
+                        setOfferToken(offerToken)
+                    }
+                }
+                .build()
+        )
+        val flowParams = BillingFlowParams.newBuilder()
+            .setProductDetailsParamsList(list)
+            .setIsOfferPersonalized(personalized ?: false)
+            .setObfuscatedProfileId(selfOrderId)
+            .setObfuscatedAccountId(userId)
+            .build()
+        mClient?.launchBillingFlow(activity, flowParams)
+    }
+
+
+    /**
+     * 手动获取交易信息(通常会 PurchasesUpdatedListener 在回调)
+     * @param   type ProductType.SUBS | ProductType.INAPP
+     */
+    suspend fun queryPurchaseAsync(type: String) {
+        log("queryPurchaseAsync type:$type")
+        val params = QueryPurchasesParams.newBuilder().setProductType(type)
+        mClient?.queryPurchasesAsync(params.build(), mPurchaseResponseListener)
+    }
+
+
+    /************************************ 核销 ***************************************/
+
+    /**
+     * 客户端核销
+     * 核销购买(商品可重复购买)
+     */
+    suspend fun consumePurchases(purchase: Purchase) {
+        val consumeParams = ConsumeParams.newBuilder()
+            .setPurchaseToken(purchase.purchaseToken)
+            .build()
+        val consumeResult = withContext(Dispatchers.IO) {
+            mClient?.consumePurchase(consumeParams)
+        }
+    }
+
+    /**
+     * 客户端核销
+     * 核销购买(商品不可重复购买)
+     */
+    suspend fun acknowledgePurchases(purchase: Purchase) {
+        if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
+            if (!purchase.isAcknowledged) {
+                val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
+                val ackPurchaseResult = withContext(Dispatchers.IO) {
+                    mClient?.acknowledgePurchase(acknowledgePurchaseParams.build(), mAcknowledgeListener)
+                }
+            }
+        }
+    }
+
+    suspend fun acknowledgeAllUnacknowledgedPurchases(type: String) {
+        val params = QueryPurchasesParams.newBuilder().setProductType(type).build()
+        mClient?.queryPurchasesAsync(params) { result, purchases ->
+            if (result.responseCode == BillingResponseCode.OK && purchases != null) {
+                purchases.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED && !it.isAcknowledged }
+                    .forEach { purchase ->
+                        val acknowledgeParams = AcknowledgePurchaseParams.newBuilder()
+                            .setPurchaseToken(purchase.purchaseToken)
+                            .build()
+                        mClient?.acknowledgePurchase(acknowledgeParams, mAcknowledgeListener)
+                    }
+            }
+        }
+    }
+
+    /************************************ 查询交易历史 ***************************************/
+
+
+    /**
+     * 提取交易结果
+     * @param map   Map<ProductId, Type>
+     */
+    suspend fun processPurchases(map: HashMap<String, String>): ProductDetailsResult? {
+        val productList = ArrayList<Product>().apply {
+            map.entries.forEach {
+                add(
+                    Product.newBuilder()
+                        .setProductId(it.key)
+                        .setProductType(it.value)
+                        .build()
+                )
+            }
+        }
+        val params = QueryProductDetailsParams.newBuilder().setProductList(productList)
+
+        // leverage queryProductDetails Kotlin extension function
+        return withContext(Dispatchers.IO) {
+            mClient?.queryProductDetails(params.build())
+        }
+    }
+
+    fun release() {
+        mHandler?.removeCallbacksAndMessages(null)
+        mHandler = null
+
+        mClient?.endConnection()
+        mClient = null
+    }
+
+    private fun log(msg: String) {
+        Log.e("GooglePayUtil", msg)
+    }
+}

+ 0 - 17
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/IPay.kt

@@ -1,17 +0,0 @@
-package com.adealink.weparty.wallet.pay
-
-import android.app.Activity
-import android.content.Intent
-import com.adealink.frame.base.Rlt
-import com.adealink.weparty.module.wallet.data.PurchaseInfo
-import com.adealink.weparty.module.wallet.data.SkuInfo
-import com.adealink.weparty.wallet.pay.model.PurchaseResult
-
-interface IPay {
-    suspend fun setup(): Rlt<Any>
-    suspend fun querySkuDetails(skuIdList: List<String>): Rlt<List<SkuInfo>>
-    suspend fun consume(token: String): Rlt<Any?>
-    suspend fun launchPaymentFlow(activity: Activity, skuInfo: SkuInfo, orderId: String): PurchaseResult
-    suspend fun queryPurchases(): Rlt<PurchaseInfo>
-    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
-}

+ 0 - 8
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/IPayListener.kt

@@ -1,8 +0,0 @@
-package com.adealink.weparty.wallet.pay
-
-import com.adealink.frame.frame.IListener
-import com.adealink.weparty.wallet.pay.model.PurchaseResult
-
-interface IPayListener: IListener {
-    fun onPendingPurchaseResult(result: PurchaseResult)
-}

+ 0 - 20
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/IPayManager.kt

@@ -1,20 +0,0 @@
-package com.adealink.weparty.wallet.pay
-
-import android.app.Activity
-import android.content.Intent
-import com.adealink.frame.base.Rlt
-import com.adealink.weparty.module.wallet.data.PayChannel
-import com.adealink.weparty.wallet.recharge.data.RechargeProduct
-
-interface IPayManager {
-    suspend fun setupPayChannel(channel: PayChannel): Rlt<Any>
-    suspend fun getRechargeList(channel: PayChannel): Rlt<List<RechargeProduct>>
-    suspend fun buyProduct(
-        activity: Activity,
-        productInfo: RechargeProduct,
-        createdOrderId: String? = null,
-    ): Rlt<Any>
-
-    fun queryAndHandleUnDealPurchases()
-    fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?)
-}

+ 0 - 28
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/PayConstants.kt

@@ -1,28 +0,0 @@
-package com.adealink.weparty.wallet.pay
-
-interface PayRes {
-    companion object {
-        const val SUCCESS = 0
-        const val SET_UP_DISCONNECTED = 1001//Lose connect with Play Store
-        const val FEATURE_NOT_SUPPORTED= 1002//The requested feature is not supported by the Play Store on the current device.
-        const val PURCHASE_SKU_UN_MATCH = 1003//Product channel does not match
-        const val CREATE_ORDER_FAIL = 1004//Create order failed
-        const val CREATE_ORDER_ID_VALID = 1005//Create order failed, order id is null or empty
-        const val PURCHASE_VALID = 1006//Purchase info is null or no purchased
-        const val PURCHASE_USER_CANCEL = 1007//Transaction was canceled by the user.
-        const val VERIFY_ORDER_FAILED = 1008//verify order failed
-        const val SKU_INVALID = 1009//No sku match product
-        const val RECHARGE_LIST_EMPTY = 1010//Recharge list is empty
-        const val NEED_QUERY_PURCHASES = 1011
-        const val PURCHASE_PENDING = 1012//Purchase pending
-        const val SERVICE_DISCONNECTED = 1014//The app is not connected to the Play Store service via the Google Play Billing Library.
-        const val SERVICE_UNAVAILABLE = 1015//The service is currently unavailable.
-        const val BILLING_UNAVAILABLE = 1016//A user billing error occurred during processing.
-        const val ITEM_UNAVAILABLE = 1017//The requested product is not available for purchase.
-        const val DEVELOPER_ERROR = 1018//Error resulting from incorrect usage of the API.
-        const val ERROR = 1019//Fatal error during the API action.
-        const val ITEM_ALREADY_OWNED = 1020//The purchase failed because the item is already owned.
-        const val ITEM_NOT_OWNED = 1021//Requested action on the item failed since it is not owned by the user.
-        const val NETWORK_ERROR = 1022//此错误表示设备和 Play 系统之间的网络连接出现问题,若要恢复,请使用简单的重试策略或指数退避算法
-    }
-}

+ 0 - 474
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/PayManager.kt

@@ -1,474 +0,0 @@
-package com.adealink.weparty.wallet.pay
-
-import android.app.Activity
-import android.content.Intent
-import com.adealink.frame.base.IError
-import com.adealink.frame.base.Rlt
-import com.adealink.frame.coroutine.dispatcher.Dispatcher
-import com.adealink.frame.frame.BaseFrame
-import com.adealink.frame.frame.IListener
-import com.adealink.weparty.App
-import com.adealink.weparty.module.wallet.data.PayChannel
-import com.adealink.weparty.module.wallet.data.PayError
-import com.adealink.weparty.module.wallet.data.Purchase
-import com.adealink.weparty.wallet.data.CONSUME_OPEN
-import com.adealink.weparty.wallet.data.VERIFY_MAX_RETRY_TIME
-import com.adealink.weparty.wallet.data.VERIFY_OPEN
-import com.adealink.weparty.wallet.datasource.remote.WalletHttpService
-import com.adealink.weparty.wallet.pay.model.PurchaseResult
-import com.adealink.weparty.wallet.recharge.data.RechargeProduct
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-
-val payManager: IPayManager by lazy { PayManager() }
-
-class PayManager : IPayManager, IPayListener, BaseFrame<IListener>() {
-
-    private val googleBilling by lazy {
-        GoogleBilling().apply {
-            addListener(this@PayManager)
-        }
-    }
-
-    private val walletHttpService by lazy {
-        App.instance.networkService.getHttpService(WalletHttpService::class.java)
-    }
-
-    private var productInfoListCache: List<RechargeProduct>? = null
-    private var lowestRechargeMoney: String? = null
-
-    private fun getPay(channel: PayChannel): IPay {
-        return when (channel) {
-            PayChannel.Google -> googleBilling
-        }
-    }
-
-    override suspend fun setupPayChannel(channel: PayChannel): Rlt<Any> {
-        return getPay(channel).setup()
-    }
-
-    override suspend fun getRechargeList(channel: PayChannel): Rlt<List<RechargeProduct>> {
-        return Rlt.Failed(IError(""))
-//        val getRechargeListStatEvent = PayStatEvent(PayStatEvent.Action.GET_RECHARGE_LIST).apply {
-//            type to getReportPayType(channel)
-//        }
-//        when (val result =
-//            walletHttpService.getRechargeList(
-//                GetRechargeListReq(channelIds = listOf(channel.value))
-//            )
-//                .apply {
-//                    Log.logRltD(TAG_PAY, "getRechargeList", this)
-//                }) {
-//            is Rlt.Success -> {
-//                val res = result.data.data
-//                var resultList = listOf<ProductInfo>()
-//                val productInfoList =
-//                    res?.channelRechargeMap?.get(ProductConfigChannel.Default.value)?.productInfos
-//                if (productInfoList.isNullOrEmpty()) {
-//                    //套餐列表为空,上报失败
-//                    getRechargeListStatEvent.apply {
-//                        this.result to CommonEventValue.Result.FAILED
-//                        code to PayRes.RECHARGE_LIST_EMPTY
-//                    }.send()
-//                    return Rlt.Failed(RechargeListEmptyError())
-//                }
-//                getRechargeListStatEvent.apply {
-//                    this.result to CommonEventValue.Result.SUCCESS
-//                }.send()
-//                //获取三方套餐列表
-//                val getThirdSkuListStatEvent =
-//                    PayStatEvent(PayStatEvent.Action.GET_THIRD_SKU_LIST).apply {
-//                        type to getReportPayType(channel)
-//                    }
-//                val skuListRlt = getPay(channel).querySkuDetails(productInfoList.map { it.skuId })
-//                val skuInfoList = mutableListOf<SkuInfo>()
-//                when (skuListRlt) {
-//                    is Rlt.Success -> {
-//                        getThirdSkuListStatEvent.apply {
-//                            this.result to CommonEventValue.Result.SUCCESS
-//                        }.send()
-//                        skuListRlt.data.forEach { skuInfo ->
-//                            skuInfoList.add(skuInfo)
-//                        }
-//                        resultList = getDisplayProductList(productInfoList, skuInfoList)
-//                    }
-//                    is Rlt.Failed -> {
-//                        getThirdSkuListStatEvent.apply {
-//                            this.result to CommonEventValue.Result.FAILED
-//                            code to skuListRlt.error.serverCode
-//                        }.send()
-//                        return skuListRlt
-//                    }
-//                }
-//                productInfoListCache = resultList
-//                return Rlt.Success(resultList)
-//            }
-//            is Rlt.Failed -> {
-//                getRechargeListStatEvent.apply {
-//                    result to CommonEventValue.Result.FAILED
-//                    code to result.error.serverCode
-//                }.send()
-//                return result
-//            }
-//        }
-    }
-
-//    private fun getDisplayProductList(
-//        originProductInfo: List<ProductInfo>,
-//        skuInfoList: List<SkuInfo>?,
-//    ): List<ProductInfo> {
-//        val displayProductList = mutableListOf<ProductInfo>()
-//        originProductInfo.forEach { productInfo ->
-//            val skuInfo = skuInfoList?.find { it.getSkuId() == productInfo.skuId }
-//            if (skuInfo != null) {
-//                productInfo.skuId = skuInfo.getSkuId()
-//                productInfo.priceAmountCents = skuInfo.getPriceAmountMicros() / 10000
-//                productInfo.priceCurrencyCode = skuInfo.getPriceCurrencyCode()
-//                productInfo.channel = skuInfo.payChannel.value
-//                displayProductList.add(productInfo)
-//            }
-//        }
-//        return displayProductList
-//    }
-
-//    private suspend fun createOrder(productInfo: ProductInfo): Rlt<String> {
-//        val createOrderStatEvent = PayStatEvent(PayStatEvent.Action.CREATE_ORDER).apply {
-//            type to getReportPayType(productInfo.channel)
-//            source to SOURCE_BUY
-//        }
-//        val orderId: String?
-//        val createOrderRlt =
-//            walletHttpService.createOrder(
-//                CreateOrderReq(
-//                    productId = "${productInfo.productId}",
-//                    orderProductType = productInfo.productType,
-//                    channel = productInfo.channel,
-//                    currencyCode = productInfo.priceCurrencyCode,
-//                    price = String.format(Locale.US,"%.2f", productInfo.priceAmountCents.toFloat() / 100)
-//                        .toFloatOrNull()
-//                )
-//            )
-//        Log.logRltD(TAG_PAY, "createOrder", createOrderRlt)
-//        if (createOrderRlt !is Rlt.Success) {
-//            createOrderStatEvent.apply {
-//                result to CommonEventValue.Result.FAILED
-//                code to (createOrderRlt as? Rlt.Failed)?.error?.serverCode
-//            }.send()
-//            return Rlt.Failed(PayError(PayRes.CREATE_ORDER_FAIL))
-//        }
-//        orderId = createOrderRlt.data.data?.orderId
-//        if (orderId.isNullOrEmpty()) {
-//            createOrderStatEvent.apply {
-//                result to CommonEventValue.Result.FAILED
-//                code to PayRes.CREATE_ORDER_ID_VALID
-//            }.send()
-//            return Rlt.Failed(PayError(PayRes.CREATE_ORDER_ID_VALID))
-//        }
-//        createOrderStatEvent.apply {
-//            result to CommonEventValue.Result.SUCCESS
-//            transactionId to orderId
-//        }.send()
-//        return Rlt.Success(orderId)
-//    }
-
-    override suspend fun buyProduct(
-        activity: Activity,
-        productInfo: RechargeProduct,
-        createdOrderId: String?,
-    ): Rlt<Any> {
-        return Rlt.Failed(IError(""))
-//        val skuInfo = when (val skuRlt = querySku(productInfo)) {
-//            is Rlt.Success -> {
-//                skuRlt.data
-//            }
-//            is Rlt.Failed -> {
-//                return skuRlt
-//            }
-//        }
-//        if (productInfo.lackCurrencyInfo()) {
-//            productInfo.priceAmountCents = skuInfo.getPriceAmountMicros() / 10000
-//            productInfo.priceCurrencyCode = skuInfo.getPriceCurrencyCode()
-//        }
-//        var orderId: String? = createdOrderId
-//        if (orderId.isNullOrEmpty()) {
-//            //创建订单
-//            val createOrderRlt = createOrder(productInfo)
-//            if (createOrderRlt !is Rlt.Success) {
-//                return createOrderRlt
-//            }
-//            orderId = createOrderRlt.data
-//        }
-//        val channel =
-//            PayChannel.map(productInfo.channel) ?: return Rlt.Failed(PayChannelUnKnowError())
-//        //发起支付
-//        val payStatEvent = PayStatEvent(PayStatEvent.Action.PAY).apply {
-//            type to getReportPayType(productInfo.channel)
-//            source to SOURCE_BUY
-//            transactionId to orderId
-//        }
-//        val purchaseResult = getPay(channel).launchPaymentFlow(activity, skuInfo, orderId)
-//        val purchase =
-//            purchaseResult.purchaseInfo?.purchases?.firstOrNull { it.getSkuId() == skuInfo.getSkuId() }
-//        payStatEvent.apply {
-//            externalOrderId to purchase?.getOrderId()
-//            code to purchaseResult.resCode
-//            op to when (purchase?.isPurchased()) {
-//                true -> "purchased"
-//                else -> ""
-//            }
-//        }
-//        if (purchaseResult.resCode != PayRes.SUCCESS) {
-//            if (purchase?.isPurchased() == true) {
-//                //如果已购买这个商品
-//                return onPurchasedResult(
-//                    productInfo,
-//                    purchase,
-//                    orderId,
-//                    SOURCE_BUY
-//                )
-//            }
-//            payStatEvent.apply {
-//                result to CommonEventValue.Result.FAILED
-//                code to purchaseResult.resCode
-//            }.send()
-//            return Rlt.Failed(PayError(purchaseResult.resCode))
-//        }
-//        val obfuscatedAccountId = purchase?.obfuscatedAccountId()
-//        if (!obfuscatedAccountId.isNullOrEmpty() && obfuscatedAccountId != AccountModule.uid.toString()) {
-//            //非当前账户订单不处理
-//            payStatEvent.apply {
-//                result to CommonEventValue.Result.FAILED
-//                code to PayRes.PURCHASE_VALID
-//            }.send()
-//            return Rlt.Failed(PayError(PayRes.PURCHASE_VALID))
-//        }
-//        if (purchase?.isPending() == true) {
-//            payStatEvent.apply {
-//                result to CommonEventValue.Result.FAILED
-//                code to PayRes.PURCHASE_PENDING
-//            }.send()
-//            return Rlt.Failed(PayError(PayRes.PURCHASE_PENDING, msg = getCompatString(R.string.wallet_buy_pending)))
-//        }
-//        if (purchase == null || !purchase.isPurchased()) {
-//            payStatEvent.apply {
-//                result to CommonEventValue.Result.FAILED
-//                code to PayRes.PURCHASE_VALID
-//            }.send()
-//            return Rlt.Failed(PayError(PayRes.PURCHASE_VALID))
-//        }
-//        payStatEvent.apply {
-//            result to CommonEventValue.Result.SUCCESS
-//        }.send()
-//        return onPurchasedResult(
-//            productInfo,
-//            purchase,
-//            orderId,
-//            SOURCE_BUY
-//        )
-    }
-
-//    private suspend fun querySku(productInfo: ProductInfo): Rlt<SkuInfo> {
-//        val channel =
-//            PayChannel.map(productInfo.channel) ?: return Rlt.Failed(PayChannelUnKnowError())
-//        return when (val skuListRlt = getPay(channel).querySkuDetails(listOf(productInfo.skuId))) {
-//            is Rlt.Success -> {
-//                val skuInfo = skuListRlt.data.getOrNull(0)
-//                    ?: return Rlt.Failed(PayError(PayRes.SKU_INVALID))
-//                Rlt.Success(skuInfo)
-//            }
-//            is Rlt.Failed -> {
-//                skuListRlt
-//            }
-//        }
-//    }
-
-    private suspend fun onPurchasedResult(
-        productInfo: RechargeProduct,
-        purchase: Purchase,
-        orderId: String,
-        fromSource: Int,
-    ): Rlt<Any> {
-        if (!VERIFY_OPEN) {
-            return Rlt.Failed(PayError(PayRes.VERIFY_ORDER_FAILED))
-        }
-
-//        val channel =
-//            PayChannel.map(productInfo.channel) ?: return Rlt.Failed(PayChannelUnKnowError())
-//        //验证订单
-//        val innerOrderId = orderId.ifEmpty { purchase.obfuscatedProfileId() } ?: ""
-//        var verifySuccess = false
-//        val verifyOrderStatEvent = PayStatEvent(PayStatEvent.Action.VERIFY_ORDER).apply {
-//            type to getReportPayType(productInfo.channel)
-//            source to fromSource
-//            transactionId to innerOrderId
-//            externalOrderId to purchase.getOrderId()
-//            obfuscatedAccountId to purchase.obfuscatedAccountId()
-//            obfuscatedProfileId to purchase.obfuscatedProfileId()
-//        }
-
-        loop1@ for (i in 1..VERIFY_MAX_RETRY_TIME) {
-//            val verifyOrderRlt = walletHttpService.verifyOrder(
-//                VerifyOrderReq(
-//                    purchaseToken = purchase.getPurchaseToken(),
-//                    packageName = PackageUtil.getPackageName(),
-//                    channel = productInfo.channel,
-//                    skuId = productInfo.skuId,
-//                    currency = productInfo.priceCurrencyCode,
-//                    amount = String.format(Locale.US, "%.2f", productInfo.priceAmountCents.toFloat() / 100)
-//                        .toFloatOrNull()
-//                )
-//            )
-//            Log.logRltI(TAG_PAY, "verifyOrder", verifyOrderRlt)
-//            if (verifyOrderRlt is Rlt.Success) {
-//                verifySuccess = true
-//                verifyOrderStatEvent.apply {
-//                    result to CommonEventValue.Result.SUCCESS
-//                }.send()
-//                reportPurchaseSuccess(productInfo, innerOrderId, purchase.getOrderId())
-//                break@loop1
-//            }
-//            verifyOrderStatEvent.apply {
-//                result to CommonEventValue.Result.FAILED
-//                code to (verifyOrderRlt as? Rlt.Failed)?.error?.serverCode
-//            }.send()
-        }
-//        if (!verifySuccess) {
-//            return Rlt.Failed(PayError(PayRes.VERIFY_ORDER_FAILED))
-//        }
-
-        if (CONSUME_OPEN) {
-            //消费订单
-//            val consumeRlt = getPay(channel).consume(purchase.getPurchaseToken())
-//            PayStatEvent(PayStatEvent.Action.CONSUME_ORDER).apply {
-//                type to getReportPayType(productInfo.channel)
-//                transactionId to innerOrderId
-//                externalOrderId to purchase.getOrderId()
-//                source to fromSource
-//                result to when(consumeRlt) {
-//                    is Rlt.Success -> CommonEventValue.Result.SUCCESS
-//                    else -> CommonEventValue.Result.FAILED
-//                }
-//                code to (consumeRlt as? Rlt.Failed)?.error?.serverCode
-//            }.send()
-        }
-//        if (fromSource == SOURCE_COMPENSATE) {
-//            walletManager.fetchCurrency()
-//        }
-        return Rlt.Success(Any())
-    }
-
-    private val reportedOrderIdSet by lazy {
-        MutableStateFlow<Set<String>>(emptySet())
-    }
-//    private fun reportPurchaseSuccess(productInfo: RechargeProduct, innerOrderId: String, externalOrderId: String) {
-//        Dispatcher.wenextThreadPoolExecutor.submit {
-//            val addReportedSuccess = run {
-//                var added = false
-//                reportedOrderIdSet.update { old ->
-//                    if (externalOrderId in old) {
-//                        old // 已存在,不修改
-//                    } else {
-//                        added = true
-//                        old + externalOrderId // 不存在就新增
-//                    }
-//                }
-//                added
-//            }
-//            if (addReportedSuccess) {
-//                //非重复订单才上报
-//                StandardStatEvent.reportPurchase(
-//                    productInfo.generateReportProductInfo(),
-//                    innerOrderId,
-//                    externalOrderId
-//                )
-//                ProfileModule.reportPhoneModel(FromScene.CHARGE.scene)
-//            }
-//        }
-//    }
-
-    override fun queryAndHandleUnDealPurchases() {
-        queryAndHandleUnDealPurchases(PayChannel.Google)
-    }
-
-    private fun queryAndHandleUnDealPurchases(channel: PayChannel) {
-//        CoroutineScope(Dispatcher.WENEXT_THREAD_POOL).launch {
-//            val setupRlt = getPay(channel).setup()
-//            if (setupRlt !is Rlt.Success) {
-//                return@launch
-//            }
-//            //查询订单
-//            val purchasesRlt = getPay(channel).queryPurchases()
-//            Log.logRltD(TAG_PAY, "queryPurchases", purchasesRlt)
-////            PayStatEvent(PayStatEvent.Action.QUERY_PURCHASE).apply {
-////                result to when (purchasesRlt) {
-////                    is Rlt.Success -> CommonEventValue.Result.SUCCESS
-////                    else -> CommonEventValue.Result.FAILED
-////                }
-////                code to (purchasesRlt as? Rlt.Failed)?.error?.serverCode
-////            }.send()
-////            if (purchasesRlt !is Rlt.Success) {
-////                return@launch
-////            }
-//            handleUnDealPurchases(channel, purchasesRlt.data.purchases)
-//        }
-    }
-
-    private suspend fun handleUnDealPurchases(channel: PayChannel, purchases: List<Purchase>) {
-//        purchases.forEach { purchase ->
-//            val obfuscatedAccountId = purchase.obfuscatedAccountId()
-//            if (!obfuscatedAccountId.isNullOrEmpty() && obfuscatedAccountId != AccountModule.uid.toString()) {
-//                //非当前账户订单不处理
-//                return@forEach
-//            }
-//            if (purchase.isPurchased().not()) {
-//                return@forEach
-//            }
-//            if (purchase.isAcknowledged()) {
-//                //已经确认,直接消费
-//                val consumeRlt = getPay(channel).consume(purchase.getPurchaseToken())
-////                PayStatEvent(PayStatEvent.Action.CONSUME_ORDER).apply {
-////                    type to getReportPayType(channel)
-////                    this.result to when (consumeRlt) {
-////                        is Rlt.Success -> CommonEventValue.Result.SUCCESS
-////                        else -> CommonEventValue.Result.FAILED
-////                    }
-////                    transactionId to purchase.obfuscatedProfileId()
-////                    externalOrderId to purchase.getOrderId()
-////                    code to (consumeRlt as? Rlt.Failed)?.error?.serverCode
-////                    source to SOURCE_COMPENSATE
-////                }.send()
-//                return@forEach
-//            }
-//            //未确认,重走订单验证
-//            val productInfo: ProductInfo? =
-//                if (productInfoListCache?.isNotEmpty() == true) {
-//                    productInfoListCache?.find { it.skuId == purchase.getSkuId() }
-//                } else {
-//                    //拉取套餐列表,获取该sku对应的内部商品
-//                    val productListRlt = getRechargeList(channel)
-//                    if (productListRlt !is Rlt.Success) {
-//                        return@forEach
-//                    }
-//                    productInfoListCache?.find { it.skuId == purchase.getSkuId() }
-//                }
-//            if (productInfo == null) {
-//                return@forEach
-//            }
-//            onPurchasedResult(productInfo, purchase, "", SOURCE_COMPENSATE)
-//        }
-    }
-
-    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
-        getPay(PayChannel.Google).onActivityResult(requestCode, resultCode, intent)
-    }
-
-    override fun onPendingPurchaseResult(result: PurchaseResult) {
-        if (result.resCode == PayRes.SUCCESS && result.purchaseInfo?.purchases?.isNotEmpty() == true) {
-            CoroutineScope(Dispatcher.WENEXT_THREAD_POOL).launch {
-                handleUnDealPurchases(PayChannel.Google, result.purchaseInfo.purchases)
-            }
-        }
-    }
-
-}

+ 41 - 0
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/WeakHandler.kt

@@ -0,0 +1,41 @@
+package com.adealink.weparty.wallet.pay
+
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import java.lang.ref.WeakReference
+
+/**
+ * @author  null
+ * @date    2024/1/13 22:01
+ * @desc
+ *
+ * Everything will be alright in the end, so if it's not alright then it's not the end.
+ */
+open class WeakHandler<T>(
+    obj: T,
+    private val callback: IOnHandlerCallback<T>,
+    private val loop: Looper? = null
+) : Handler(loop ?: Looper.getMainLooper()) {
+    private val weakHandler: WeakReference<T> = WeakReference(obj)
+    private val weakData: T
+        get() = weakHandler.get() as T
+
+    override fun handleMessage(msg: Message) {
+        super.handleMessage(msg)
+        callback.onMessage(weakData, msg, this)
+    }
+
+    /**
+     * BaseHandler 消息回调callback
+     * @param
+     */
+    interface IOnHandlerCallback<T> {
+        /**
+         * 回调方法
+         * @param t      创建 BaseHandler 时传入的第一个参数
+         * @param msg    handler 原始消息
+         */
+        fun onMessage(t: T, msg: Message?, instance: Handler)
+    }
+}

+ 0 - 38
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/model/GooglePurchase.kt

@@ -1,38 +0,0 @@
-package com.adealink.weparty.wallet.pay.model
-
-import com.android.billingclient.api.Purchase
-
-data class GooglePurchase(val purchase: Purchase) : com.adealink.weparty.module.wallet.data.Purchase {
-
-    override fun isPurchased(): Boolean {
-        return purchase.purchaseState == Purchase.PurchaseState.PURCHASED
-    }
-
-    override fun isPending(): Boolean {
-        return purchase.purchaseState == Purchase.PurchaseState.PENDING
-    }
-
-    override fun getSkuId(): String {
-        return purchase.products.getOrNull(0) ?: ""
-    }
-
-    override fun getOrderId(): String {
-        return purchase.orderId ?: ""
-    }
-
-    override fun getPurchaseToken(): String {
-        return purchase.purchaseToken
-    }
-
-    override fun isAcknowledged(): Boolean {
-        return purchase.isAcknowledged
-    }
-
-    override fun obfuscatedAccountId(): String? {
-        return purchase.accountIdentifiers?.obfuscatedAccountId
-    }
-
-    override fun obfuscatedProfileId(): String? {
-        return purchase.accountIdentifiers?.obfuscatedProfileId
-    }
-}

+ 0 - 44
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/model/GoogleSkuInfo.kt

@@ -1,44 +0,0 @@
-package com.adealink.weparty.wallet.pay.model
-
-import com.adealink.weparty.module.wallet.data.PayChannel
-import com.adealink.weparty.module.wallet.data.SkuInfo
-import com.android.billingclient.api.ProductDetails
-import com.android.billingclient.api.SkuDetails
-
-data class GoogleSkuInfo(
-    override val payChannel: PayChannel = PayChannel.Google,
-    val skuDetails: ProductDetails? = null,
-    val skuDetailsOld: SkuDetails? = null,
-) : SkuInfo {
-
-    override fun getSkuId(): String {
-        if (skuDetails != null) {
-            return skuDetails.productId
-        }
-        if (skuDetailsOld != null) {
-            return skuDetailsOld.sku
-        }
-        return ""
-    }
-
-    override fun getPriceAmountMicros(): Long {
-        if (skuDetails != null) {
-            return skuDetails.oneTimePurchaseOfferDetails?.priceAmountMicros ?: 0
-        }
-        if (skuDetailsOld != null) {
-            return skuDetailsOld.priceAmountMicros
-        }
-        return 0
-    }
-
-    override fun getPriceCurrencyCode(): String {
-        if (skuDetails != null) {
-            return skuDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode ?: ""
-        }
-        if (skuDetailsOld != null) {
-            return skuDetailsOld.priceCurrencyCode
-        }
-        return ""
-    }
-
-}

+ 0 - 5
module/wallet/src/main/java/com/adealink/weparty/wallet/pay/model/PurchaseResult.kt

@@ -1,5 +0,0 @@
-package com.adealink.weparty.wallet.pay.model
-
-import com.adealink.weparty.module.wallet.data.PurchaseInfo
-
-data class PurchaseResult(val resCode: Int, val purchaseInfo: PurchaseInfo?)

+ 1 - 1
module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/adapter/RechargeItemViewBinder.kt

@@ -58,7 +58,7 @@ class RechargeItemViewBinder(
             holder.binding.vBg.background = null
         }
         holder.binding.tvAmount.text = item.data.amount.toString()
-        holder.binding.tvSpend.text = "IDR ${item.data.spend}"
+        holder.binding.tvSpend.text = "${item.data.currency} ${item.data.price}"
 
     }
 

+ 28 - 5
module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/data/WalletData.kt

@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
 
 data class RechargeConfigReq(
     @SerializedName("walletType") val currency: Int,
-    @SerializedName("rechargePlatform") val platform: Int = 2 //0:Official, 1:Android, 2:Browser
+    @SerializedName("rechargePlatform") val platform: Int = 1 //0:Official, 1:Android, 2:Browser
 )
 
 data class RechargeConfigRes(
@@ -13,10 +13,22 @@ data class RechargeConfigRes(
 )
 
 data class RechargeProduct(
-    @SerializedName("id") val id: String,
-    @SerializedName("coinRechargeAmount") val amount: Int,
-    @SerializedName("amount") val spend: Double
-)
+    @SerializedName("id") val id: String, //用作服务端下单
+    @SerializedName("coinRechargeAmount") val amount: Int, //充值数量
+    @SerializedName("amount") val price: Double, //价格
+    @SerializedName("currency") val currency: String, //法币名称
+    @SerializedName("code") val productId: String, //用作查询Google充值档位
+){
+    companion object{
+        val INVALID_PRODUCT = RechargeProduct(
+            "invalid",
+            0,
+            0.0,
+            "",
+            ""
+        )
+    }
+}
 
 data class RechargeItemData(
     val data: RechargeProduct,
@@ -27,4 +39,15 @@ data class ConvertCurrencyReq(
     @SerializedName("fromWalletType") val fromCurrency: Int,
     @SerializedName("toWalletType") val toCurrency: Int,
     @SerializedName("amount") val amount: Double,
+)
+
+/**
+ * 预下单
+ */
+data class PrePurchaseReq(
+    @SerializedName("id") val id: String,
+)
+
+data class PrePurchaseRes(
+    @SerializedName("result") val result: String,
 )

+ 5 - 2
module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/fragment/CoinRechargeFragment.kt

@@ -49,7 +49,7 @@ class CoinRechargeFragment : BaseFragment(R.layout.fragment_recharge_coin) {
         binding.vCoinCard.btnConvert.onClick {
             convertToDiamond()
         }
-        binding.btnTopUp.onClick {
+        binding.btnRecharge.onClick {
             goRecharge()
         }
     }
@@ -82,12 +82,15 @@ class CoinRechargeFragment : BaseFragment(R.layout.fragment_recharge_coin) {
     }
 
     private fun goRecharge() {
+        val act = activity?:return
         val selectItem = rechargeViewModel.getSelectedItem()
         if (selectItem == null) {
             showToast(R.string.wallet_recharge_not_selected)
             return
         }
         showLoading()
-        walletViewModel.recharge(selectItem.data)
+        walletViewModel.recharge(act, selectItem.data).observe(viewLifecycleOwner){
+            dismissLoading()
+        }
     }
 }

+ 2 - 2
module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/fragment/DiamondRechargeFragment.kt

@@ -87,7 +87,7 @@ class DiamondRechargeFragment : BaseFragment(R.layout.fragment_recharge_diamond)
             showToast(R.string.wallet_recharge_not_selected)
             return
         }
-        showLoading()
-        walletViewModel.recharge(selectItem.data)
+//        showLoading()
+//        walletViewModel.recharge(selectItem.data)
     }
 }

+ 7 - 7
module/wallet/src/main/java/com/adealink/weparty/wallet/recharge/viewmodel/RechargeViewModel.kt

@@ -8,7 +8,7 @@ import com.adealink.weparty.module.wallet.data.Currency
 import com.adealink.weparty.wallet.datasource.remote.WalletHttpService
 import com.adealink.weparty.wallet.recharge.data.RechargeConfigReq
 import com.adealink.weparty.wallet.recharge.data.RechargeItemData
-import com.adealink.weparty.wallet.recharge.data.RechargeProduct
+import com.adealink.weparty.wallet.recharge.data.RechargeProduct.Companion.INVALID_PRODUCT
 import kotlinx.coroutines.launch
 
 class RechargeViewModel : BaseViewModel() {
@@ -58,12 +58,12 @@ class RechargeViewModel : BaseViewModel() {
 
     companion object {
         private val EMPTY_RECHARGE_LIST = listOf(
-            RechargeItemData(RechargeProduct("", 0, 0.0)),
-            RechargeItemData(RechargeProduct("", 0, 0.0)),
-            RechargeItemData(RechargeProduct("", 0, 0.0)),
-            RechargeItemData(RechargeProduct("", 0, 0.0)),
-            RechargeItemData(RechargeProduct("", 0, 0.0)),
-            RechargeItemData(RechargeProduct("", 0, 0.0))
+            RechargeItemData(INVALID_PRODUCT),
+            RechargeItemData(INVALID_PRODUCT),
+            RechargeItemData(INVALID_PRODUCT),
+            RechargeItemData(INVALID_PRODUCT),
+            RechargeItemData(INVALID_PRODUCT),
+            RechargeItemData(INVALID_PRODUCT),
         )
     }
 }

+ 189 - 4
module/wallet/src/main/java/com/adealink/weparty/wallet/viewmodel/WalletViewModel.kt

@@ -1,29 +1,60 @@
 package com.adealink.weparty.wallet.viewmodel
 
+import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.AppBase
+import com.adealink.frame.base.CommonDataNullError
+import com.adealink.frame.base.IError
 import com.adealink.frame.base.Rlt
+import com.adealink.frame.data.json.toJsonErrorNull
+import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.livedata.OnceMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+import com.adealink.frame.util.PackageUtil
+import com.adealink.weparty.App
+import com.adealink.weparty.commonui.ext.isFailure
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.module.wallet.data.Currency
 import com.adealink.weparty.module.wallet.viewmodel.IWalletViewModel
+import com.adealink.weparty.wallet.R
+import com.adealink.weparty.wallet.data.TAG_WALLET
+import com.adealink.weparty.wallet.datasource.remote.WalletHttpService
 import com.adealink.weparty.wallet.manager.IWalletListener
 import com.adealink.weparty.wallet.manager.walletManager
+import com.adealink.weparty.wallet.pay.GooglePayManager
+import com.adealink.weparty.wallet.pay.GooglePayUtil
+import com.adealink.weparty.wallet.recharge.data.PrePurchaseReq
 import com.adealink.weparty.wallet.recharge.data.RechargeProduct
+import com.android.billingclient.api.BillingClient.BillingResponseCode
+import com.android.billingclient.api.BillingResult
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.Purchase
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
 
-class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener {
+class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener,
+    GooglePayUtil.OnPurchasesListener {
+
+    private val walletHttpService by lazy {
+        App.instance.networkService.getHttpService(WalletHttpService::class.java)
+    }
     override val coinLD: LiveData<Double> = MutableLiveData()
     override val diamondLD: LiveData<Double> = MutableLiveData()
     override val beanLD: LiveData<Double> = MutableLiveData()
 
     init {
         walletManager.addListener(this)
+        GooglePayManager.addListener(this)
     }
 
     override fun onCleared() {
         super.onCleared()
         walletManager.removeListener(this)
+        GooglePayManager.removeListener(this)
     }
 
     fun getWalletData() {
@@ -90,13 +121,167 @@ class WalletViewModel : BaseViewModel(), IWalletViewModel, IWalletListener {
         return liveData
     }
 
-    fun recharge(data: RechargeProduct): LiveData<Rlt<Any>> {
+    /**
+     * 充值流程
+     * 1. 预下单
+     * 2. 查询充值套餐
+     * 3.
+     */
+    fun recharge(activity: FragmentActivity, data: RechargeProduct): LiveData<Rlt<Any>> {
         val liveData = OnceMutableLiveData<Rlt<Any>>()
         viewModelScope.launch {
-            liveData.send(
-                walletManager.recharge(data)
+            Log.i(TAG_WALLET, "recharge, $data")
+            val uid = ProfileModule.getMyUid()
+            if (uid.isNullOrEmpty()) {
+                liveData.send(Rlt.Failed(IError("User not login")))
+                return@launch
+            }
+
+            //1. 预下单
+            val prePurchaseRlt = prePurchase(data.id)
+            Log.i(TAG_WALLET, "recharge, 1.prePurchase, rlt:$prePurchaseRlt")
+            if (prePurchaseRlt.isFailure) {
+                liveData.send(prePurchaseRlt)
+                return@launch
+            }
+            val orderId = (prePurchaseRlt as Rlt.Success).data
+
+            //2. 查询充值套餐
+            val productDetailRlt = queryProductDetail(data.productId)
+            Log.i(TAG_WALLET, "recharge, 2.queryProductDetail, rlt:$productDetailRlt")
+            if (productDetailRlt.isFailure) {
+                liveData.send(prePurchaseRlt)
+                return@launch
+            }
+
+            val productDetail = (productDetailRlt as Rlt.Success).data
+            //3.支付
+            Log.i(
+                TAG_WALLET,
+                "recharge, 3.purchases, orderId:${orderId}, productDetail:$productDetail"
             )
+            purchases(activity, uid, orderId, productDetail)
         }
         return liveData
     }
+
+    private suspend fun prePurchase(productId: String): Rlt<String> {
+        val rlt = walletHttpService.prePurchase(PrePurchaseReq(productId))
+        when (rlt) {
+            is Rlt.Failed -> {
+                return rlt
+            }
+
+            is Rlt.Success -> {
+                val orderId = rlt.data.data?.result
+                if (orderId.isNullOrEmpty()) {
+                    return Rlt.Failed(CommonDataNullError())
+                }
+                return Rlt.Success(orderId)
+            }
+        }
+    }
+
+    private suspend fun queryProductDetail(productId: String): Rlt<ProductDetails> {
+        return suspendCancellableCoroutine { continuation ->
+            val payClient = GooglePayManager.getGooglePayClient()
+            if (payClient == null) {
+                if (continuation.isActive) {
+                    continuation.resume(Rlt.Failed(IError(msg = "App error")))
+                }
+                return@suspendCancellableCoroutine
+            }
+
+            payClient?.queryProductDetails(
+                "${PackageUtil.getPackageName().replace(".debug", "")}.$productId",
+                GooglePayUtil.TYPE_IN_APP
+            ) { result, list ->
+                Log.i(
+                    TAG_WALLET,
+                    "queryProductDetail:\nresult:$result\n productDetail:${toJsonErrorNull(list)}"
+                )
+                if (result.responseCode != BillingResponseCode.OK) {
+                    if (continuation.isActive) {
+                        continuation.resume(Rlt.Failed(IError(msg = result.debugMessage)))
+                    }
+                    return@queryProductDetails
+                }
+                if (list.productDetailsList.isEmpty()) {
+                    if (continuation.isActive) {
+                        continuation.resume(Rlt.Failed(IError(msg = getCompatString(R.string.wallet_query_recharge_product_fail))))
+                    }
+                    return@queryProductDetails
+                }
+
+                if (AppBase.isRelease) {
+                    Log.i(
+                        TAG_WALLET,
+                        "queryProductDetails:\nresult:$result\n productDetail:${toJsonErrorNull(list)}"
+                    )
+                } else {
+                    Log.i(
+                        TAG_WALLET,
+                        "queryProductDetails:\nresult:$result\n productDetail:${toJsonErrorNull(list)}"
+                    )
+                }
+
+                val productDetail = list.productDetailsList.getOrNull(0)
+                if (productDetail == null) {
+                    if (continuation.isActive) {
+                        continuation.resume(Rlt.Failed(IError(msg = getCompatString(R.string.wallet_query_recharge_product_fail))))
+                    }
+                    return@queryProductDetails
+                }
+
+                if (continuation.isActive) {
+                    continuation.resume(Rlt.Success(productDetail))
+                }
+            }
+        }
+    }
+
+    private suspend fun purchases(
+        activity: FragmentActivity,
+        uid: String,
+        orderId: String,
+        productDetail: ProductDetails
+    ) {
+        return suspendCancellableCoroutine { continuation ->
+            GooglePayManager.getGooglePayClient()
+                ?.purchases(activity, productDetail, uid, orderId)
+        }
+    }
+
+    override fun onPurchasesSuccess(purchases: List<Purchase>) {
+        Log.d(
+            "zhangfei", "onPurchasesSuccess, ${
+                purchases.joinToString(separator = ",", transform = {
+                    it.orderId.toString()
+                })
+            }"
+        )
+        showToast("onPurchasesSuccess")
+    }
+
+    override fun onPurchasesCancel(purchases: List<Purchase>?) {
+        Log.d(
+            "zhangfei", "onPurchasesCancel, ${
+                purchases?.joinToString(separator = ",", transform = {
+                    it.orderId.toString()
+                })
+            }"
+        )
+        showToast("onPurchasesCancel")
+    }
+
+    override fun onPurchasesFailure(result: BillingResult, purchases: List<Purchase>?) {
+        Log.d(
+            "zhangfei", "onPurchasesFailure, ${
+                purchases?.joinToString(separator = ",", transform = {
+                    it.orderId.toString()
+                })
+            }"
+        )
+        showToast("onPurchasesFailure")
+    }
 }

+ 5 - 5
module/wallet/src/main/res/layout/fragment_recharge_coin.xml

@@ -42,7 +42,7 @@
                 app:layout_constraintTop_toTopOf="parent" />
 
             <com.adealink.weparty.commonui.widget.CommonButton
-                android:id="@+id/btn_top_up"
+                android:id="@+id/btn_recharge"
                 android:layout_width="0dp"
                 android:layout_height="@dimen/common_button_height"
                 android:layout_marginTop="20dp"
@@ -61,14 +61,14 @@
                 android:ellipsize="end"
                 android:fontFamily="@font/poppins_semibold"
                 android:includeFontPadding="false"
-                android:text="@string/wallet_topup_instruction_title"
+                android:text="@string/wallet_recharge_instruction_title"
                 android:textColor="@color/color_FF1D2129"
                 android:textSize="12sp"
                 app:layout_constrainedWidth="true"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintHorizontal_bias="0"
                 app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/btn_top_up" />
+                app:layout_constraintTop_toBottomOf="@id/btn_recharge" />
 
             <androidx.appcompat.widget.AppCompatTextView
                 android:id="@+id/tv_instruction1"
@@ -76,7 +76,7 @@
                 android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:includeFontPadding="false"
-                android:text="@string/wallet_topup_instruction_1"
+                android:text="@string/wallet_recharge_instruction_1"
                 android:textColor="@color/color_FF1D2129"
                 android:textSize="12sp"
                 app:layout_constrainedWidth="true"
@@ -91,7 +91,7 @@
                 android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:includeFontPadding="false"
-                android:text="@string/wallet_topup_instruction_2"
+                android:text="@string/wallet_recharge_instruction_2"
                 android:textColor="@color/color_FF1D2129"
                 android:textSize="12sp"
                 app:layout_constrainedWidth="true"

+ 3 - 3
module/wallet/src/main/res/layout/fragment_recharge_diamond.xml

@@ -61,7 +61,7 @@
                 android:ellipsize="end"
                 android:fontFamily="@font/poppins_semibold"
                 android:includeFontPadding="false"
-                android:text="@string/wallet_topup_instruction_title"
+                android:text="@string/wallet_recharge_instruction_title"
                 android:textColor="@color/color_FF1D2129"
                 android:textSize="12sp"
                 app:layout_constrainedWidth="true"
@@ -76,7 +76,7 @@
                 android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:includeFontPadding="false"
-                android:text="@string/wallet_topup_instruction_1"
+                android:text="@string/wallet_recharge_instruction_1"
                 android:textColor="@color/color_FF1D2129"
                 android:textSize="12sp"
                 app:layout_constrainedWidth="true"
@@ -91,7 +91,7 @@
                 android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:includeFontPadding="false"
-                android:text="@string/wallet_topup_instruction_2"
+                android:text="@string/wallet_recharge_instruction_2"
                 android:textColor="@color/color_FF1D2129"
                 android:textSize="12sp"
                 app:layout_constrainedWidth="true"

+ 4 - 3
module/wallet/src/main/res/values-in/strings.xml

@@ -15,9 +15,9 @@
     <string name="wallet_coin_convert_to_diamond"><![CDATA[Konversi ke Berlian >]]></string>
     <string name="wallet_diamond_convert_to_coin"><![CDATA[Konversi ke Koin >]]></string>
     <string name="wallet_withdraw_cash"><![CDATA[Penarikan Tunai >]]></string>
-    <string name="wallet_topup_instruction_title">Instruksi Isi Ulang</string>
-    <string name="wallet_topup_instruction_1">1. Setelah isi ulang berhasil, pasti akan ada penundaan dalam kedatangan dana di akun Anda. Mohon bersabar.</string>
-    <string name="wallet_topup_instruction_2">2. Jika Anda mengalami masalah selama proses pengisian ulang, silakan klik "Umpan Balik".</string>
+    <string name="wallet_recharge_instruction_title">Instruksi Isi Ulang</string>
+    <string name="wallet_recharge_instruction_1">1. Setelah isi ulang berhasil, pasti akan ada penundaan dalam kedatangan dana di akun Anda. Mohon bersabar.</string>
+    <string name="wallet_recharge_instruction_2">2. Jika Anda mengalami masalah selama proses pengisian ulang, silakan klik "Umpan Balik".</string>
     <string name="wallet_recharge_not_selected">Silakan pilih paket pengisian ulang.</string>
     <string name="wallet_convert_currency_title">Konversi ke %s</string>
     <string name="wallet_all_convert_button">Semua</string>
@@ -29,4 +29,5 @@
     <string name="wallet_convert_from_bean">Dari Kacang</string>
     <string name="wallet_convert_to_bean">Ke Kacang</string>
     <string name="wallet_convert_please_fill">Silakan isi</string>
+    <string name="wallet_query_recharge_product_fail">Paket isi ulang tidak ditemukan, silakan hubungi kami untuk menyelesaikan masalah ini.</string>
 </resources>

+ 4 - 3
module/wallet/src/main/res/values-zh/strings.xml

@@ -15,9 +15,9 @@
     <string name="wallet_coin_convert_to_diamond"><![CDATA[转换为钻石 >]]></string>
     <string name="wallet_diamond_convert_to_coin"><![CDATA[转换为金币 >]]></string>
     <string name="wallet_withdraw_cash"><![CDATA[提现 >]]></string>
-    <string name="wallet_topup_instruction_title">充值说明</string>
-    <string name="wallet_topup_instruction_1">1.充值成功后,账户会有延迟到账,请耐心等待;</string>
-    <string name="wallet_topup_instruction_2">2.如果您在充值过程中遇到任何问题,请点击“反馈”</string>
+    <string name="wallet_recharge_instruction_title">充值说明</string>
+    <string name="wallet_recharge_instruction_1">1.充值成功后,账户会有延迟到账,请耐心等待;</string>
+    <string name="wallet_recharge_instruction_2">2.如果您在充值过程中遇到任何问题,请点击“反馈”</string>
     <string name="wallet_recharge_not_selected">请选择充值套餐</string>
     <string name="wallet_convert_currency_title">兑换为 %s</string>
     <string name="wallet_all_convert_button">全部兑换</string>
@@ -29,4 +29,5 @@
     <string name="wallet_convert_from_bean">从金豆转换</string>
     <string name="wallet_convert_to_bean">转换为金豆</string>
     <string name="wallet_convert_please_fill">请填写</string>
+    <string name="wallet_query_recharge_product_fail">查询不到该充值套餐,请联系我们解决</string>
 </resources>

+ 4 - 3
module/wallet/src/main/res/values/strings.xml

@@ -15,9 +15,9 @@
     <string name="wallet_coin_convert_to_diamond"><![CDATA[Convert to Diamonds >]]></string>
     <string name="wallet_diamond_convert_to_coin"><![CDATA[Convert to Coin >]]></string>
     <string name="wallet_withdraw_cash"><![CDATA[Withdraw Crash >]]></string>
-    <string name="wallet_topup_instruction_title">Recharge Instructions</string>
-    <string name="wallet_topup_instruction_1">1. After a successful top-up, there will definitely be a delay in the funds arriving in your account. Please wait patiently.</string>
-    <string name="wallet_topup_instruction_2">2. If you encounter any problems during the recharge process, please click "Feedback".</string>
+    <string name="wallet_recharge_instruction_title">Recharge Instructions</string>
+    <string name="wallet_recharge_instruction_1">1. After a successful top-up, there will definitely be a delay in the funds arriving in your account. Please wait patiently.</string>
+    <string name="wallet_recharge_instruction_2">2. If you encounter any problems during the recharge process, please click "Feedback".</string>
     <string name="wallet_recharge_not_selected">Please select recharge package.</string>
     <string name="wallet_convert_currency_title">Convert to %s</string>
     <string name="wallet_all_convert_button">All</string>
@@ -29,4 +29,5 @@
     <string name="wallet_convert_from_bean">From Bean</string>
     <string name="wallet_convert_to_bean">To Bean</string>
     <string name="wallet_convert_please_fill">Please fill in</string>
+    <string name="wallet_query_recharge_product_fail">Cannot find the recharge package, please contact us to resolve the issue.</string>
 </resources>