Skip to content
Merged
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.38/Dart 3.10.
* 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',
);
Comment on lines +209 to +212

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In Dart, class names should follow the UpperCamelCase convention. It seems Sk2PurchaseParam might be a typo and should be SK2PurchaseParam. Please verify the correct class name and update the example for consistency with Dart's style guide.

Suggested change
final purchaseParamSk2 = Sk2PurchaseParam(
productDetails: productDetails,
winBackOfferId: 'your_win_back_offer_id',
);
final purchaseParamSk2 = SK2PurchaseParam(
productDetails: productDetails,
winBackOfferId: 'your_win_back_offer_id',
);
References
  1. The repository style guide (line 10) requires following language-specific style guides. For Dart, this includes using UpperCamelCase for class names as per the Effective Dart style guide. (link)


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.10.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.38/Dart 3.10.
* 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,16 @@ abstract class InAppPurchasePlatform extends PlatformInterface {
/// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a
/// purchase is pending for completion.
///
/// > [!WARNING]
/// > On iOS/macOS, 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.
/// > On Android, If you do not call [completePurchase] for a transaction on Android, Google Play
/// > will automatically refund and revoke the purchase after 3 days.
///
/// 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.10.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.4.10

* 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.9

* Add support for offer codes in StoreKit 2.
Expand All @@ -6,6 +11,7 @@
## 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 @@ -547,6 +547,33 @@ 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)
}
@available(iOS 16.0, macOS 15.0, *)
func testRedeemCodeSheetFailsGracefullyWhenNoWindow() {
let expectation = self.expectation(
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.9
version: 0.4.10

environment:
sdk: ^3.10.0
Expand Down
Loading