Skip to content
Draft
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
3 changes: 2 additions & 1 deletion packages/in_app_purchase/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
## NEXT
## 3.2.4

* Updates minimum supported SDK version to Flutter 3.35/Dart 3.9.
* Updates README to reflect currently supported OS versions for the latest
versions of the endorsed platform implementations.
* Applications built with older versions of Flutter will continue to
use compatible versions of the platform implementations.
* Clarifies `completePurchase` usage and the consequences of unfinished transactions in the README and docstrings.

## 3.2.3
* Updates minimum `in_app_purchase_storekit` version to 0.4.0.
Expand Down
24 changes: 23 additions & 1 deletion packages/in_app_purchase/in_app_purchase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ can start using the plugin. Two basic options are available:

1. A generic, idiomatic Flutter API: [in_app_purchase](https://pub.dev/documentation/in_app_purchase/latest/in_app_purchase/in_app_purchase-library.html).
This API supports most use cases for loading and making purchases.

> **NOTE**: On iOS and macOS, the generic API uses StoreKit 2 by default. If you need to fall back to StoreKit 1, call `InAppPurchaseStoreKitPlatform.enableStoreKit1()` before registering the platform.

2. Platform-specific Dart APIs: [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_storekit/latest/store_kit_wrappers/store_kit_wrappers-library.html)
and [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase_android/latest/billing_client_wrappers/billing_client_wrappers-library.html).
Expand Down Expand Up @@ -196,6 +198,24 @@ if (_isConsumable(productDetails)) {
// Updates will be delivered to the `InAppPurchase.instance.purchaseStream`.
```

StoreKit 2 Specific Purchases (iOS/macOS)
When StoreKit 2 is enabled, you can use Sk2PurchaseParam to include StoreKit 2 specific parameters such as win-back offer identifiers or promotional offers with signatures.

```dart
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';

final productDetails = ...; // Obtained from queryProductDetails

final purchaseParamSk2 = Sk2PurchaseParam(
productDetails: productDetails,
winBackOfferId: 'your_win_back_offer_id',
);

await InAppPurchase.instance.buyNonConsumable(
purchaseParam: purchaseParamSk2,
);
```

### Completing a purchase

The `InAppPurchase.purchaseStream` will send purchase updates after initiating
Expand All @@ -209,7 +229,9 @@ purchase and the store can proceed to finalize the transaction and bill
the end user's payment account.

> **Warning:** Failure to call `InAppPurchase.completePurchase` and
> get a successful response within 3 days of the purchase will result a refund.
> get a successful response within 3 days of the purchase will result in a refund on Android. On iOS/macOS (using StoreKit 1 or StoreKit 2), failing to complete purchases causes the transactions to remain in Apple's unfinished transaction queue, which has two major side effects:
> 1. The transactions will be repeatedly re-delivered on the `purchaseStream` on every app launch.
> 2. Any subsequent attempt to purchase the same product ID will immediately fail with an error indicating a duplicate transaction is pending.

### Upgrading or downgrading an existing in-app subscription

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ class InAppPurchase implements InAppPurchasePlatformAdditionProvider {
/// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a
/// purchase is pending for completion.
///
/// iOS/macOS Warning:
/// If you do not call [completePurchase] for a transaction, that transaction
/// will remain in Apple's unfinished transaction queue. This has two consequences:
/// 1. The transaction will be repeatedly re-delivered on the [purchaseStream]
/// every time the app is restarted.
/// 2. Any subsequent attempts to buy the same product ID will fail with a purchase
/// error indicating a duplicate transaction is pending.
///
/// The method will throw a [PurchaseException] when the purchase could not be
/// finished. Depending on the [PurchaseException.errorCode] the developer
/// should try to complete the purchase via this method again, or retry the
Expand Down
2 changes: 1 addition & 1 deletion packages/in_app_purchase/in_app_purchase/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: in_app_purchase
description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play.
repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
version: 3.2.3
version: 3.2.4

environment:
sdk: ^3.9.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## NEXT
## 1.4.1

* Updates minimum supported SDK version to Flutter 3.35/Dart 3.9.
* Clarifies `completePurchase` usage and the consequences of unfinished transactions in the README and docstrings.

## 1.4.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ abstract class InAppPurchasePlatform extends PlatformInterface {
/// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a
/// purchase is pending for completion.
///
/// **iOS/macOS Warning:**
/// If you do not call [completePurchase] for a transaction, that transaction
/// will remain in Apple's unfinished transaction queue. This has two consequences:
/// 1. The transaction will be repeatedly re-delivered on the [purchaseStream]
/// every time the app is restarted.
/// 2. Any subsequent attempts to buy the same product ID will fail with a purchase
/// error indicating a duplicate transaction is pending.
///
/// The method will throw a [PurchaseException] when the purchase could not be
/// finished. Depending on the [PurchaseException.errorCode] the developer
/// should try to complete the purchase via this method again, or retry the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/in_app_purcha
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
version: 1.4.0
version: 1.4.1

environment:
sdk: ^3.9.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
## 0.4.8+2

* Clarifies `completePurchase` usage and the consequences of unfinished transactions in the README and API docstrings.
* Prevents duplicate purchase attempts in StoreKit 2 by throwing a `storekit_duplicate_product_object` error when a product already has an unfinished transaction.

## 0.4.8+1

* Fixes StoreKit 2 purchase flow to send cancelled/pending/unverified results to `purchaseStream`.

## 0.4.8

* Fixes an issue causing StoreKit2 purchases to be reported as `restored` and left in an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ extension InAppPurchasePlugin: InAppPurchase2API {
}
}

for await verificationResult in Transaction.unfinished {
switch verificationResult {
case .verified(let transaction):
if transaction.productID == id {
let error = PigeonError(
code: "storekit_duplicate_product_object",
message:
"There is a pending transaction for the same product identifier. Please either wait for it to be finished or finish it manually using `completePurchase` to avoid edge cases.",
details: id)
return completion(.failure(error))
}
case .unverified:
break
}
}

let result = try await product.purchase(options: purchaseOptions)

switch result {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -733,11 +733,11 @@ void SetUpFIAInAppPurchaseAPIWithSuffix(id<FlutterBinaryMessenger> binaryMesseng
binaryMessenger:binaryMessenger
codec:FIAGetMessagesCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(startProductRequestProductIdentifiers:
completion:)],
@"FIAInAppPurchaseAPI api (%@) doesn't respond to "
@"@selector(startProductRequestProductIdentifiers:completion:)",
api);
NSCAssert(
[api respondsToSelector:@selector(startProductRequestProductIdentifiers:completion:)],
@"FIAInAppPurchaseAPI api (%@) doesn't respond to "
@"@selector(startProductRequestProductIdentifiers:completion:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray<id> *args = message;
NSArray<NSString *> *arg_productIdentifiers = GetNullableObjectAtIndex(args, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<MacroExpansion>
Expand Down Expand Up @@ -73,6 +74,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,4 +547,31 @@ final class InAppPurchase2PluginTests: XCTestCase {
await fulfillment(of: [expectation], timeout: 5)
}

func testDuplicatePurchaseFails() async throws {
let firstPurchaseExpectation = self.expectation(description: "First purchase should succeed")
let secondPurchaseExpectation = self.expectation(description: "Second purchase should fail")

plugin.purchase(id: "consumable", options: nil) { result in
switch result {
case .success:
firstPurchaseExpectation.fulfill()
case .failure(let error):
XCTFail("First purchase should NOT fail. Failed with \(error)")
}
}
await fulfillment(of: [firstPurchaseExpectation], timeout: 5)

plugin.purchase(id: "consumable", options: nil) { result in
switch result {
case .success:
XCTFail("Second purchase should NOT succeed because a transaction is already pending.")
case .failure(let error as PigeonError):
XCTAssertEqual(error.code, "storekit_duplicate_product_object")
secondPurchaseExpectation.fulfill()
case .failure(let error):
XCTFail("Unexpected error type: \(error)")
}
}
await fulfillment(of: [secondPurchaseExpectation], timeout: 5)
}
}
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.8+1
version: 0.4.8+2

environment:
sdk: ^3.9.0
Expand Down
Loading