Skip to content

[In_app_purchase_storekit] Do not throw PigeonError when a transaction is pending / cancelled / unverified #9627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.4.4

* Adds a new case `.unverified` to enum `SK2ProductPurchaseResult`
* Fixes the StoreKit2 implementation throwing `PlatformException`s instead of returning the corresponding
`SK2ProductPurchaseResult` when a purchase is cancelled / unverified / pending.

## 0.4.3

* Adds **Introductory Offer Eligibility** support for StoreKit2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,32 +86,14 @@ extension InAppPurchasePlugin: InAppPurchase2API {

switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
self.sendTransactionUpdate(
transaction: transaction, receipt: verification.jwsRepresentation)
completion(.success(result.convertToPigeon()))
case .unverified(_, let error):
completion(.failure(error))
}
case .pending:
completion(
.failure(
PigeonError(
code: "storekit2_purchase_pending",
message:
"This transaction is still pending and but may complete in the future. If it completes, it will be delivered via `purchaseStream`",
details: "Product ID : \(id)")))
case .userCancelled:
completion(
.failure(
PigeonError(
code: "storekit2_purchase_cancelled",
message: "This transaction has been cancelled by the user.",
details: "Product ID : \(id)")))
sendTransactionUpdate(
transaction: verification.unsafePayloadValue, receipt: verification.jwsRepresentation)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation unsafe seems to mean that the transaction is potentially unverified.

case .pending, .userCancelled:
break
@unknown default:
fatalError("An unknown StoreKit PurchaseResult has been encountered.")
}
completion(.success(result.convertToPigeon()))
} catch {
completion(.failure(error))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,11 @@ extension SK2PriceLocaleMessage: Equatable {
@available(iOS 15.0, macOS 12.0, *)
extension Product.PurchaseResult {
func convertToPigeon() -> SK2ProductPurchaseResultMessage {
switch self {
case .success(_):
return SK2ProductPurchaseResultMessage.success
case .userCancelled:
return SK2ProductPurchaseResultMessage.userCancelled
case .pending:
return SK2ProductPurchaseResultMessage.pending
return switch self {
case .success(.verified): .success
case .success(.unverified): .unverified
case .userCancelled: .userCancelled
case .pending: .pending
@unknown default:
fatalError()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import Foundation
Expand Down Expand Up @@ -169,8 +169,9 @@ enum SK2SubscriptionPeriodUnitMessage: Int {

enum SK2ProductPurchaseResultMessage: Int {
case success = 0
case userCancelled = 1
case pending = 2
case unverified = 1
case userCancelled = 2
case pending = 3
}

/// Generated class from Pigeon that represents data sent in messages.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ final class InAppPurchase2PluginTests: XCTestCase {
let expectation = self.expectation(description: "Purchase request should succeed")
plugin.purchase(id: "consumable", options: nil) { result in
switch result {
case .success:
case .success(let message):
XCTAssert(message == .success)
expectation.fulfill()
case .failure(let error):
XCTFail("Purchase should NOT fail. Failed with \(error)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,11 @@ final class StoreKit2TranslatorTests: XCTestCase {
let pigeonMessage = locale.convertToPigeon
XCTAssertEqual(pigeonMessage, productMessage.priceLocale)
}

func testPigeonConversionForPurchaseResult() {
// Unfortunately the .success case is not testable because the Transaction
// type has no visible initializers.
XCTAssertEqual(Product.PurchaseResult.pending.convertToPigeon(), .pending)
XCTAssertEqual(Product.PurchaseResult.userCancelled.convertToPigeon(), .userCancelled)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

Expand Down Expand Up @@ -79,6 +79,7 @@ enum SK2SubscriptionPeriodUnitMessage {

enum SK2ProductPurchaseResultMessage {
success,
unverified,
userCancelled,
pending,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ enum SK2ProductType {
nonRenewable,

/// An auto-renewable subscription.
autoRenewable;
autoRenewable,
}

extension on SK2ProductTypeMessage {
Expand Down Expand Up @@ -119,12 +119,13 @@ class SK2SubscriptionOffer {
extension on SK2SubscriptionOfferMessage {
SK2SubscriptionOffer convertFromPigeon() {
return SK2SubscriptionOffer(
id: id,
price: price,
type: type.convertFromPigeon(),
period: period.convertFromPigeon(),
periodCount: periodCount,
paymentMode: paymentMode.convertFromPigeon());
id: id,
price: price,
type: type.convertFromPigeon(),
period: period.convertFromPigeon(),
periodCount: periodCount,
paymentMode: paymentMode.convertFromPigeon(),
);
}
}

Expand Down Expand Up @@ -196,7 +197,7 @@ enum SK2SubscriptionPeriodUnit {
month,

/// A subscription period unit of a year.
year
year,
}

extension on SK2SubscriptionPeriodUnitMessage {
Expand Down Expand Up @@ -224,7 +225,7 @@ enum SK2SubscriptionOfferPaymentMode {
payUpFront,

/// A payment mode of a product discount that indicates a free trial offer.
freeTrial;
freeTrial,
}

extension on SK2SubscriptionOfferPaymentModeMessage {
Expand Down Expand Up @@ -255,28 +256,35 @@ class SK2PriceLocale {
/// Convert this instance of [SK2PriceLocale] to [SK2PriceLocaleMessage]
SK2PriceLocaleMessage convertToPigeon() {
return SK2PriceLocaleMessage(
currencyCode: currencyCode, currencySymbol: currencySymbol);
currencyCode: currencyCode,
currencySymbol: currencySymbol,
);
}
}

extension on SK2PriceLocaleMessage {
SK2PriceLocale convertFromPigeon() {
return SK2PriceLocale(
currencyCode: currencyCode, currencySymbol: currencySymbol);
currencyCode: currencyCode,
currencySymbol: currencySymbol,
);
}
}

/// Wrapper around [PurchaseResult]
/// https://developer.apple.com/documentation/storekit/product/purchaseresult
enum SK2ProductPurchaseResult {
/// The purchase succeeded and results in a transaction.
/// The purchase succeeded and results in a transaction signed by the App Store.
success,

/// The purchase succeeded but the transation could not be verified.
unverified,

/// The user canceled the purchase.
userCancelled,

/// The purchase is pending, and requires action from the customer.
pending
pending,
}

/// Wrapper around [PurchaseOption]
Expand Down Expand Up @@ -315,14 +323,16 @@ class SK2ProductPurchaseOptions {

extension on SK2ProductPurchaseResultMessage {
SK2ProductPurchaseResult convertFromPigeon() {
switch (this) {
case SK2ProductPurchaseResultMessage.success:
return SK2ProductPurchaseResult.success;
case SK2ProductPurchaseResultMessage.userCancelled:
return SK2ProductPurchaseResult.userCancelled;
case SK2ProductPurchaseResultMessage.pending:
return SK2ProductPurchaseResult.pending;
}
return switch (this) {
SK2ProductPurchaseResultMessage.success =>
SK2ProductPurchaseResult.success,
SK2ProductPurchaseResultMessage.userCancelled =>
SK2ProductPurchaseResult.userCancelled,
SK2ProductPurchaseResultMessage.pending =>
SK2ProductPurchaseResult.pending,
SK2ProductPurchaseResultMessage.unverified =>
SK2ProductPurchaseResult.unverified,
};
}
}

Expand Down Expand Up @@ -371,8 +381,9 @@ class SK2Product {
/// If any of the identifiers are invalid or can't be found, they are excluded
/// from the returned list.
static Future<List<SK2Product>> products(List<String> identifiers) async {
final List<SK2ProductMessage?> productsMsg =
await _hostApi.products(identifiers);
final List<SK2ProductMessage?> productsMsg = await _hostApi.products(
identifiers,
);
if (productsMsg.isEmpty && identifiers.isNotEmpty) {
throw PlatformException(
code: 'storekit_no_response',
Expand All @@ -389,8 +400,10 @@ class SK2Product {
/// Wrapper for StoreKit's [Product.purchase]
/// https://developer.apple.com/documentation/storekit/product/3791971-purchase
/// Initiates a purchase for the product with the App Store and displays the confirmation sheet.
static Future<SK2ProductPurchaseResult> purchase(String id,
{SK2ProductPurchaseOptions? options}) async {
static Future<SK2ProductPurchaseResult> purchase(
String id, {
SK2ProductPurchaseOptions? options,
}) async {
SK2ProductPurchaseResultMessage result;
if (options != null) {
result = await _hostApi.purchase(id, options: options.convertToPigeon());
Expand All @@ -402,12 +415,8 @@ class SK2Product {

/// Checks if the user is eligible for an introductory offer.
/// The product must be an auto-renewable subscription.
static Future<bool> isIntroductoryOfferEligible(
String productId,
) async {
final bool result = await _hostApi.isIntroductoryOfferEligible(
productId,
);
static Future<bool> isIntroductoryOfferEligible(String productId) async {
final bool result = await _hostApi.isIntroductoryOfferEligible(productId);

return result;
}
Expand All @@ -428,26 +437,28 @@ class SK2Product {
/// Converts this instance of [SK2Product] to it's pigeon representation [SK2ProductMessage]
SK2ProductMessage convertToPigeon() {
return SK2ProductMessage(
id: id,
displayName: displayName,
description: description,
price: price,
displayPrice: displayPrice,
type: type.convertToPigeon(),
priceLocale: priceLocale.convertToPigeon());
id: id,
displayName: displayName,
description: description,
price: price,
displayPrice: displayPrice,
type: type.convertToPigeon(),
priceLocale: priceLocale.convertToPigeon(),
);
}
}

extension on SK2ProductMessage {
SK2Product convertFromPigeon() {
return SK2Product(
id: id,
displayName: displayName,
displayPrice: displayPrice,
price: price,
description: description,
type: type.convertFromPigeon(),
subscription: subscription?.convertFromPigeon(),
priceLocale: priceLocale.convertFromPigeon());
id: id,
displayName: displayName,
displayPrice: displayPrice,
price: price,
description: description,
type: type.convertFromPigeon(),
subscription: subscription?.convertFromPigeon(),
priceLocale: priceLocale.convertFromPigeon(),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ class SK2ErrorMessage {
final Map<String, Object>? userInfo;
}

enum SK2ProductPurchaseResultMessage { success, userCancelled, pending }
enum SK2ProductPurchaseResultMessage {
success,
unverified,
userCancelled,
pending
}

@HostApi(dartHostTestHandler: 'TestInAppPurchase2Api')
abstract class InAppPurchase2API {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: in_app_purchase_storekit
description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
version: 0.4.3
version: 0.4.4

environment:
sdk: ^3.6.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers
// ignore_for_file: avoid_relative_lib_imports
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;

import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:in_app_purchase_storekit/src/sk2_pigeon.g.dart';

class _PigeonCodec extends StandardMessageCodec {
Expand Down