Showing
7 changed files
with
165 additions
and
1 deletions
| @@ -307,6 +307,8 @@ interface PlatformHostApi { | @@ -307,6 +307,8 @@ interface PlatformHostApi { | ||
| 307 | fun requestAppleProductInfo(productId: String, baseUnit: Long, callback: (Result<AppleProductInfo?>) -> Unit) | 307 | fun requestAppleProductInfo(productId: String, baseUnit: Long, callback: (Result<AppleProductInfo?>) -> Unit) |
| 308 | /** 从服务器下单后请求苹果支付 */ | 308 | /** 从服务器下单后请求苹果支付 */ |
| 309 | fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit) | 309 | fun performApplePayment(productId: String, uuid: String, callback: (Result<AppleProductPaymentResult?>) -> Unit) |
| 310 | + /** 恢复购买 */ | ||
| 311 | + fun performRestore(callback: (Result<Boolean>) -> Unit) | ||
| 310 | 312 | ||
| 311 | companion object { | 313 | companion object { |
| 312 | /** The codec used by PlatformHostApi. */ | 314 | /** The codec used by PlatformHostApi. */ |
| @@ -411,6 +413,24 @@ interface PlatformHostApi { | @@ -411,6 +413,24 @@ interface PlatformHostApi { | ||
| 411 | channel.setMessageHandler(null) | 413 | channel.setMessageHandler(null) |
| 412 | } | 414 | } |
| 413 | } | 415 | } |
| 416 | + run { | ||
| 417 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.performRestore$separatedMessageChannelSuffix", codec) | ||
| 418 | + if (api != null) { | ||
| 419 | + channel.setMessageHandler { _, reply -> | ||
| 420 | + api.performRestore{ result: Result<Boolean> -> | ||
| 421 | + val error = result.exceptionOrNull() | ||
| 422 | + if (error != null) { | ||
| 423 | + reply.reply(PlatformApiPigeonUtils.wrapError(error)) | ||
| 424 | + } else { | ||
| 425 | + val data = result.getOrNull() | ||
| 426 | + reply.reply(PlatformApiPigeonUtils.wrapResult(data)) | ||
| 427 | + } | ||
| 428 | + } | ||
| 429 | + } | ||
| 430 | + } else { | ||
| 431 | + channel.setMessageHandler(null) | ||
| 432 | + } | ||
| 433 | + } | ||
| 414 | } | 434 | } |
| 415 | } | 435 | } |
| 416 | } | 436 | } |
| @@ -26,6 +26,15 @@ enum StoreKitError: Error, LocalizedError { | @@ -26,6 +26,15 @@ enum StoreKitError: Error, LocalizedError { | ||
| 26 | } | 26 | } |
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | + | ||
| 30 | +struct IAPRestoreTransactions: Codable { | ||
| 31 | + var product_id: String? | ||
| 32 | + var transaction_id: String? | ||
| 33 | + var original_transaction_id: String? | ||
| 34 | + var app_account_token: String? | ||
| 35 | + var web_order_line_item_id: String? | ||
| 36 | +} | ||
| 37 | + | ||
| 29 | class ApplePayment { | 38 | class ApplePayment { |
| 30 | var delegate: AppDelegate? | 39 | var delegate: AppDelegate? |
| 31 | 40 | ||
| @@ -217,6 +226,47 @@ class ApplePayment { | @@ -217,6 +226,47 @@ class ApplePayment { | ||
| 217 | } | 226 | } |
| 218 | } | 227 | } |
| 219 | 228 | ||
| 229 | + private func verifyRestoreWithServer(transaction: IAPRestoreTransactions) async -> Bool { | ||
| 230 | + guard AppShared.shared.isLogin else { | ||
| 231 | + print("Apple payment restore verify skipped: missing login info") | ||
| 232 | + return false | ||
| 233 | + } | ||
| 234 | + | ||
| 235 | + guard let baseURL = URL(string: AppShared.shared.baseUrl), | ||
| 236 | + let url = URL(string: "/client/doublefeel/payment/order_restore/apple/", relativeTo: baseURL)?.absoluteURL else { | ||
| 237 | + print("Apple payment restore verify failed: invalid baseUrl \(AppShared.shared.baseUrl)") | ||
| 238 | + return false | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + var request = URLRequest(url: url) | ||
| 242 | + request.httpMethod = "POST" | ||
| 243 | + request.timeoutInterval = 30 | ||
| 244 | + request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
| 245 | + request.setValue("application/json", forHTTPHeaderField: "Accept") | ||
| 246 | + request.setValue(AppShared.shared.userSummary?.accessToken, forHTTPHeaderField: "access_token") | ||
| 247 | + request.setValue(AppShared.shared.agent.finalUA, forHTTPHeaderField: "User-Agent") | ||
| 248 | + | ||
| 249 | + do { | ||
| 250 | + request.httpBody = try JSONEncoder().encode(transaction) | ||
| 251 | + let (data, response) = try await URLSession.shared.data(for: request) | ||
| 252 | + guard let httpResponse = response as? HTTPURLResponse else { | ||
| 253 | + print("Apple payment restore verify failed: invalid response") | ||
| 254 | + return false | ||
| 255 | + } | ||
| 256 | + | ||
| 257 | + let bodyText = String(data: data, encoding: .utf8) ?? "" | ||
| 258 | + print("Apple payment restore verify response: status=\(httpResponse.statusCode), body=\(bodyText)") | ||
| 259 | + | ||
| 260 | + guard (200..<300).contains(httpResponse.statusCode) else { | ||
| 261 | + return false | ||
| 262 | + } | ||
| 263 | + return true | ||
| 264 | + } catch { | ||
| 265 | + print("Apple payment restore verify failed: \(error.localizedDescription)") | ||
| 266 | + return false | ||
| 267 | + } | ||
| 268 | + } | ||
| 269 | + | ||
| 220 | private func paymentResult( | 270 | private func paymentResult( |
| 221 | productId: String, | 271 | productId: String, |
| 222 | appAccountToken: String? = nil, | 272 | appAccountToken: String? = nil, |
| @@ -241,6 +291,42 @@ class ApplePayment { | @@ -241,6 +291,42 @@ class ApplePayment { | ||
| 241 | } | 291 | } |
| 242 | return error.localizedDescription | 292 | return error.localizedDescription |
| 243 | } | 293 | } |
| 294 | + | ||
| 295 | + func restore() async -> Bool { | ||
| 296 | + var transactions = [Transaction]() | ||
| 297 | + for await result in Transaction.currentEntitlements { | ||
| 298 | + if case let .verified(trans) = result { | ||
| 299 | + | ||
| 300 | + if trans.appAccountToken != nil && | ||
| 301 | + trans.revocationDate == nil && | ||
| 302 | + trans.productType == .autoRenewable { | ||
| 303 | + transactions.append(trans) | ||
| 304 | + }else { | ||
| 305 | + await trans.finish() | ||
| 306 | + } | ||
| 307 | + } | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + guard let restoreTransaction = transactions.compactMap({ IAPRestoreTransactions( | ||
| 311 | + product_id: $0.productID, | ||
| 312 | + transaction_id: "\($0.id)", | ||
| 313 | + original_transaction_id: "\($0.originalID)", | ||
| 314 | + app_account_token: $0.appAccountToken?.uuidString.lowercased() ?? "", | ||
| 315 | + web_order_line_item_id: $0.webOrderLineItemID ?? "") } | ||
| 316 | + ).last else{ | ||
| 317 | + return false | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + guard await verifyRestoreWithServer(transaction: restoreTransaction) else { | ||
| 321 | + return false | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + for tran in transactions { | ||
| 325 | + await tran.finish() | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | + return true | ||
| 329 | + } | ||
| 244 | } | 330 | } |
| 245 | 331 | ||
| 246 | extension ApplePayment { | 332 | extension ApplePayment { |
| @@ -339,6 +339,8 @@ protocol PlatformHostApi { | @@ -339,6 +339,8 @@ protocol PlatformHostApi { | ||
| 339 | func requestAppleProductInfo(productId: String, baseUnit: Int64, completion: @escaping (Result<AppleProductInfo?, Error>) -> Void) | 339 | func requestAppleProductInfo(productId: String, baseUnit: Int64, completion: @escaping (Result<AppleProductInfo?, Error>) -> Void) |
| 340 | /// 从服务器下单后请求苹果支付 | 340 | /// 从服务器下单后请求苹果支付 |
| 341 | func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void) | 341 | func performApplePayment(productId: String, uuid: String, completion: @escaping (Result<AppleProductPaymentResult?, Error>) -> Void) |
| 342 | + /// 恢复购买 | ||
| 343 | + func performRestore(completion: @escaping (Result<Bool, Error>) -> Void) | ||
| 342 | } | 344 | } |
| 343 | 345 | ||
| 344 | /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. | 346 | /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. |
| @@ -437,5 +439,21 @@ class PlatformHostApiSetup { | @@ -437,5 +439,21 @@ class PlatformHostApiSetup { | ||
| 437 | } else { | 439 | } else { |
| 438 | performApplePaymentChannel.setMessageHandler(nil) | 440 | performApplePaymentChannel.setMessageHandler(nil) |
| 439 | } | 441 | } |
| 442 | + /// 恢复购买 | ||
| 443 | + let performRestoreChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.performRestore\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 444 | + if let api = api { | ||
| 445 | + performRestoreChannel.setMessageHandler { _, reply in | ||
| 446 | + api.performRestore { result in | ||
| 447 | + switch result { | ||
| 448 | + case .success(let res): | ||
| 449 | + reply(wrapResult(res)) | ||
| 450 | + case .failure(let error): | ||
| 451 | + reply(wrapError(error)) | ||
| 452 | + } | ||
| 453 | + } | ||
| 454 | + } | ||
| 455 | + } else { | ||
| 456 | + performRestoreChannel.setMessageHandler(nil) | ||
| 457 | + } | ||
| 440 | } | 458 | } |
| 441 | } | 459 | } |
| @@ -37,6 +37,13 @@ class PriceFormatter{ | @@ -37,6 +37,13 @@ class PriceFormatter{ | ||
| 37 | * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)` | 37 | * `{systemWebViewUA} doublefeel/{versionCode}({versionName})(Apple##Apple##{model}; iOS{osVersion}; {height}x{width})(huawei)` |
| 38 | */ | 38 | */ |
| 39 | final class PlatformHostApiImpl: PlatformHostApi { | 39 | final class PlatformHostApiImpl: PlatformHostApi { |
| 40 | + func performRestore(completion: @escaping (Result<Bool, any Error>) -> Void) { | ||
| 41 | + Task{ | ||
| 42 | + let success = await AppShared.shared.payment.restore() | ||
| 43 | + completion(.success(success)) | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + | ||
| 40 | func updateLoginInfo(jsonString: String, baseUrl: String) throws { | 47 | func updateLoginInfo(jsonString: String, baseUrl: String) throws { |
| 41 | AppShared.shared.baseUrl = baseUrl | 48 | AppShared.shared.baseUrl = baseUrl |
| 42 | if let data = jsonString.data(using: .utf8){ | 49 | if let data = jsonString.data(using: .utf8){ |
| @@ -432,4 +432,33 @@ class PlatformHostApi { | @@ -432,4 +432,33 @@ class PlatformHostApi { | ||
| 432 | return (pigeonVar_replyList[0] as AppleProductPaymentResult?); | 432 | return (pigeonVar_replyList[0] as AppleProductPaymentResult?); |
| 433 | } | 433 | } |
| 434 | } | 434 | } |
| 435 | + | ||
| 436 | + /// 恢复购买 | ||
| 437 | + Future<bool> performRestore() async { | ||
| 438 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.performRestore$pigeonVar_messageChannelSuffix'; | ||
| 439 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 440 | + pigeonVar_channelName, | ||
| 441 | + pigeonChannelCodec, | ||
| 442 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 443 | + ); | ||
| 444 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||
| 445 | + final List<Object?>? pigeonVar_replyList = | ||
| 446 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 447 | + if (pigeonVar_replyList == null) { | ||
| 448 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 449 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 450 | + throw PlatformException( | ||
| 451 | + code: pigeonVar_replyList[0]! as String, | ||
| 452 | + message: pigeonVar_replyList[1] as String?, | ||
| 453 | + details: pigeonVar_replyList[2], | ||
| 454 | + ); | ||
| 455 | + } else if (pigeonVar_replyList[0] == null) { | ||
| 456 | + throw PlatformException( | ||
| 457 | + code: 'null-error', | ||
| 458 | + message: 'Host platform returned null value for non-null return value.', | ||
| 459 | + ); | ||
| 460 | + } else { | ||
| 461 | + return (pigeonVar_replyList[0] as bool?)!; | ||
| 462 | + } | ||
| 463 | + } | ||
| 435 | } | 464 | } |
| @@ -120,4 +120,8 @@ abstract class PlatformHostApi { | @@ -120,4 +120,8 @@ abstract class PlatformHostApi { | ||
| 120 | /// 从服务器下单后请求苹果支付 | 120 | /// 从服务器下单后请求苹果支付 |
| 121 | @async | 121 | @async |
| 122 | AppleProductPaymentResult? performApplePayment(String productId, String uuid); | 122 | AppleProductPaymentResult? performApplePayment(String productId, String uuid); |
| 123 | + | ||
| 124 | + /// 恢复购买 | ||
| 125 | + @async | ||
| 126 | + bool performRestore(); | ||
| 123 | } | 127 | } |
-
Please register or login to post a comment