Commit 892450124f65e4d8669aa808167e4a2f64c7e5b3

Authored by 权海
1 parent 689df72c

feat(ui):添加restore接口

@@ -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 }
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 archiveVersion = 1; 3 archiveVersion = 1;
4 classes = { 4 classes = {
5 }; 5 };
6 - objectVersion = 77; 6 + objectVersion = 54;
7 objects = { 7 objects = {
8 8
9 /* Begin PBXBuildFile section */ 9 /* Begin PBXBuildFile section */
@@ -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 }