Sitelet https://github.com/flutter/packages/pull/12584
Skip to content

[in_app_purchase] Support StoreKit 2 introductory offer eligibility JWS - #12584

Merged
auto-submit[bot] merged 1 commit into
flutter:mainfrom
joelbg-cloud:spike/sk2-intro-offer-eligibility-jws
Sep 4, 2026
Merged

auto-submit[bot] merged 1 commit into
flutter:mainfrom
joelbg-cloud:spike/sk2-intro-offer-eligibility-jws

Conversation

@joelbg-cloud

@joelbg-cloud joelbg-cloud commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Adds an optional StoreKit 2-specific field to Sk2PurchaseParam that forwards a server-signed compact JWS to Product.PurchaseOption.introductoryOfferEligibility(compactJWS:).

Fixes flutter/flutter#191668

Problem

StoreKit 2 lets an app's own server decide, per purchase, whether an introductory offer applies. The app receives a compact JWS signed with an App Store Connect In-App Purchase key (Generating JWS to sign App Store requests) whose allowIntroductoryOffer claim Apple documents as "a Boolean value, true or false, that determines whether the customer is eligible for an introductory offer".

The plugin did not expose this purchase option, so apps whose eligibility rules live on their own server could not use the plugin's purchase path.

This is distinct from the existing isIntroductoryOfferEligible(productId), which wraps Product.SubscriptionInfo.isEligibleForIntroOffer and reads the App Store's default eligibility. That read cannot express a server-signed per-purchase decision.

Implementation

Sk2PurchaseParam(
  productDetails: productDetails,
  introductoryOfferEligibilityCompactJWS: compactJWSFromMyServer,
);

The value flows through SK2ProductPurchaseOptions and the Pigeon message to the Swift purchase-option builder:

if let compactJWS = options?.introductoryOfferEligibilityCompactJWS {
  purchaseOptions.insert(.introductoryOfferEligibility(compactJWS: compactJWS))
}

The JWS is opaque to the plugin: it is never generated, signed, parsed, validated or inspected.

Generated Pigeon files were regenerated with the repository's canonical dart run pigeon --input pigeons/sk2_pigeon.dart.

Availability

No #available gate is required. introductoryOfferEligibility(compactJWS:) is declared @backDeployed(before: iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4) with availability iOS 15.0+ / macOS 12.0+, which is the same floor as the existing @available(iOS 15.0, macOS 12.0, *) StoreKit 2 extension it is added to.

Backwards compatibility

Not breaking:

  • the field is optional and null by default
  • when null, no purchase option is added and the App Store applies its own default eligibility, so existing apps behave identically
  • the new Pigeon field is appended, so existing encoded payloads decode unchanged
  • StoreKit 1 is untouched
  • isIntroductoryOfferEligible, promotional offers, win-back offers, appAccountToken and quantity are unchanged and continue to work alongside the new option

Tests

Dart (in_app_purchase_storekit_2_platform_test.dart), all passing:

  • null JWS -> no option is sent
  • non-null JWS -> forwarded unchanged
  • JWS coexists with appAccountToken, quantity, promotionalOffer and winBackOfferId
  • a generic PurchaseParam sends no JWS

Native (InAppPurchaseStoreKit2PluginTests.swift):

  • testPurchaseWithIntroductoryOfferEligibilityJWS drives a purchase with the option set and passes.

Known limitation in local native verification

I could not run the full RunnerTests suite locally. It crashes in StoreKit2TranslatorTests.setUp() (StoreKit2TranslatorTests.swift:69) with EXC_BREAKPOINT/SIGTRAP, and the existing purchase-success tests fail with PigeonError plus an asynchronous timeout.

This is independent of this change: I reproduced the identical crash signature and the identical purchase-test failure on the unmodified upstream base (9f595f3) under the same Xcode 26.6 / iOS 26.5 simulator. It matches flutter/flutter#184678, including the same file and line and the same SKTestSession SKInternalErrorDomain Code=3 symptom.

To be precise about what was verified locally: the patch-specific native test passes, and dart format, flutter analyze, the 106 Dart unit tests and swift-format lint --strict are all clean. I am not claiming the full native suite passes. Because product lookup fails in that environment, the native test exercises the call path but cannot by itself assert the option reached StoreKit; the deterministic assertions for that are the Dart tests above, and the Swift call site was additionally verified with swiftc -typecheck against the iOS 15 target.

No unrelated upstream test was modified or worked around.

Pre-Review Checklist

Footnotes

  1. See "Known limitation in local native verification" above regarding the pre-existing native suite failure tracked in [Only on 26.3/26.4][in_app_purchase_storekit] iOS native tests failing with timeouts, SKInternalErrorDomain, and crashes flutter#184678. ↩ ↩2

@google-cla

google-cla Bot commented Aug 25, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for setting introductory offer eligibility from a server-signed compact JWS via Sk2PurchaseParam.introductoryOfferEligibilityCompactJWS in the StoreKit 2 implementation of the in_app_purchase_storekit package. The value is forwarded verbatim to StoreKit 2 as Product.PurchaseOption.introductoryOfferEligibility(compactJWS:). Corresponding Pigeon messages, platform wrappers, Swift implementation, unit tests, and integration tests have been updated to support this new parameter. I have no feedback to provide as there are no review comments.

@joelbg-cloud

Copy link
Copy Markdown
Contributor Author

@googlebot I signed it!

1 similar comment
@joelbg-cloud

Copy link
Copy Markdown
Contributor Author

@googlebot I signed it!

@Piinks Piinks added the triage-ios Should be looked at in iOS triage label Aug 25, 2026
@LouiseHsu LouiseHsu added the CICD Run CI/CD label Aug 27, 2026
@LongCatIsLooong

Copy link
Copy Markdown
Contributor

@joelbg-cloud Thanks for contributing! Looks like there are some merge conflicts, could you resolve them?

Adds an optional StoreKit 2-specific field to Sk2PurchaseParam,
`introductoryOfferEligibilityCompactJWS`, which is forwarded unchanged to
`Product.PurchaseOption.introductoryOfferEligibility(compactJWS:)`.

This lets an app's own server decide, per purchase, whether an introductory
offer applies, by signing a compact JWS with an App Store Connect In-App
Purchase key. The plugin treats the JWS as opaque: it never generates, signs,
parses, validates or inspects it.

This is distinct from the existing `isIntroductoryOfferEligible(productId)`,
which reads the App Store's default eligibility and cannot express a
server-signed per-purchase decision.

The option is optional and null by default; when null no purchase option is
added and the App Store applies its own default eligibility, so existing
behavior is unchanged. StoreKit 1 is untouched.

No availability gate is needed: the StoreKit API is back deployed and is
available on the same iOS 15.0 / macOS 12.0 floor as the surrounding
StoreKit 2 extension.

Fixes flutter/flutter#191668
@joelbg-cloud
joelbg-cloud force-pushed the spike/sk2-intro-offer-eligibility-jws branch from 2d7646e to 6375840 Compare September 3, 2026 20:19
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 3, 2026
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label Sep 3, 2026
@LouiseHsu LouiseHsu added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 3, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 3, 2026
@auto-submit

auto-submit Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/packages/12584, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

@LouiseHsu LouiseHsu added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 3, 2026
@auto-submit
auto-submit Bot merged commit 334faff into flutter:main Sep 4, 2026
13 checks passed
pull Bot pushed a commit to Mattlk13/flutter that referenced this pull request Sep 9, 2026
…er#192485)

flutter/packages@9af9c60...36e088a

2026-09-09 daniel.leon@cloudsufi.com [quick_actions] Adopt code-excerpts
for README (flutter/packages#12643)
2026-09-08 65155920+0xharkirat@users.noreply.github.com [camera_web] Fix
TypeError when reading the torch capability (flutter/packages#12647)
2026-09-08 gibbonsj97@gmail.com [google_maps_flutter_web] Avoid
replacing advanced marker content on move (flutter/packages#11952)
2026-09-08 mit@google.com [material_ui][cupertino_ui] Change issue
tracker label in pubspec.yaml (flutter/packages#12792)
2026-09-08 saurabhmirajkar000@gmail.com [material_ui] Fix FilledButton
Material 3 default style docs (flutter/packages#12620)
2026-09-08 21270878+elliette@users.noreply.github.com [infra] Use a
modified no-response workflow in flutter/packages
(flutter/packages#12745)
2026-09-08 21270878+elliette@users.noreply.github.com [material_ui]
Migrate M3 Banner template to use new gen_defaults
(flutter/packages#12734)
2026-09-08 engine-flutter-autoroll@skia.org Roll Flutter from
63170e9 to b444e78 (13 revisions) (flutter/packages#12791)
2026-09-08 fluttergithubbot@gmail.com Sync release-go_router-18.0.1 to
main (flutter/packages#12725)
2026-09-08 fluttergithubbot@gmail.com Sync release-material_ui-1.1.1 to
main (flutter/packages#12726)
2026-09-08 stuartmorgan@google.com [tool] Adopt `platform` 3.2.0
(flutter/packages#12789)
2026-09-07 50643541+Mairramer@users.noreply.github.com [material_ui] Fix
SliverGeometry maxPaintExtent assertion in CarouselView.weighted
(flutter/packages#12563)
2026-09-05 engine-flutter-autoroll@skia.org Roll Flutter from
5a6cfa7 to 63170e9 (15 revisions) (flutter/packages#12767)
2026-09-04 engine-flutter-autoroll@skia.org Manual roll Flutter from
70797e1 to 5a6cfa7 (52 revisions) (flutter/packages#12760)
2026-09-04 brackenavaron@gmail.com [material_ui] port drawer tests over
from flutter/widgets (flutter/packages#12711)
2026-09-04 tarrinneal@gmail.com add cooldown (flutter/packages#12708)
2026-09-04 joeldumasbg@gmail.com [in_app_purchase] Support StoreKit 2
introductory offer eligibility JWS (flutter/packages#12584)
2026-09-04 21270878+elliette@users.noreply.github.com [material_ui]
Migrate M3 Badge template to use new gen_defaults
(flutter/packages#12733)
2026-09-04 stuartmorgan@google.com [google_maps_flutter] Convert heatmap
controller to Swift (flutter/packages#12713)
2026-09-04 97480502+b-luk@users.noreply.github.com [material_ui] Remove
unused `maintainState` constructor parameter in `scaffold_test.dart`
(flutter/packages#12754)
2026-09-04 a1rwulf@users.noreply.github.com [video_player_avfoundation]
Route video over AirPlay (flutter/packages#12490)
2026-09-04 jerome.dellamaria@proton.me [google_fonts] Add
google_fonts_lite file to allow tree-shaking of the other huge files
(flutter/packages#11433)
2026-09-04 engine-flutter-autoroll@skia.org Roll Flutter from
0cbd1a4 to 70797e1 (27 revisions) (flutter/packages#12727)
2026-09-04 stuartmorgan@google.com Update Chrome for stable tests
(flutter/packages#12747)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC flutter-ecosystem@google.com on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
victorsanni pushed a commit to victorsanni/packages that referenced this pull request Sep 9, 2026
…WS (flutter#12584)

Adds an optional StoreKit 2-specific field to `Sk2PurchaseParam` that forwards a server-signed compact JWS to [`Product.PurchaseOption.introductoryOfferEligibility(compactJWS:)`](https://developer.apple.com/documentation/storekit/product/purchaseoption/introductoryoffereligibility(compactjws:)).

Fixes flutter/flutter#191668

## Problem

StoreKit 2 lets an app's own server decide, per purchase, whether an introductory offer applies. The app receives a compact JWS signed with an App Store Connect In-App Purchase key ([Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)) whose `allowIntroductoryOffer` claim Apple documents as "a Boolean value, `true` or `false`, that determines whether the customer is eligible for an introductory offer".

The plugin did not expose this purchase option, so apps whose eligibility rules live on their own server could not use the plugin's purchase path.

This is distinct from the existing `isIntroductoryOfferEligible(productId)`, which wraps `Product.SubscriptionInfo.isEligibleForIntroOffer` and **reads** the App Store's *default* eligibility. That read cannot express a server-signed per-purchase decision.

## Implementation

```dart
Sk2PurchaseParam(
  productDetails: productDetails,
  introductoryOfferEligibilityCompactJWS: compactJWSFromMyServer,
);
```

The value flows through `SK2ProductPurchaseOptions` and the Pigeon message to the Swift purchase-option builder:

```swift
if let compactJWS = options?.introductoryOfferEligibilityCompactJWS {
  purchaseOptions.insert(.introductoryOfferEligibility(compactJWS: compactJWS))
}
```

The JWS is opaque to the plugin: it is never generated, signed, parsed, validated or inspected.

Generated Pigeon files were regenerated with the repository's canonical `dart run pigeon --input pigeons/sk2_pigeon.dart`.

## Availability

No `#available` gate is required. `introductoryOfferEligibility(compactJWS:)` is declared `@backDeployed(before: iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4)` with availability iOS 15.0+ / macOS 12.0+, which is the same floor as the existing `@available(iOS 15.0, macOS 12.0, *)` StoreKit 2 extension it is added to.

## Backwards compatibility

Not breaking:

- the field is optional and null by default
- when null, no purchase option is added and the App Store applies its own default eligibility, so existing apps behave identically
- the new Pigeon field is appended, so existing encoded payloads decode unchanged
- StoreKit 1 is untouched
- `isIntroductoryOfferEligible`, promotional offers, win-back offers, `appAccountToken` and `quantity` are unchanged and continue to work alongside the new option

## Tests

Dart (`in_app_purchase_storekit_2_platform_test.dart`), all passing:

- null JWS -> no option is sent
- non-null JWS -> forwarded unchanged
- JWS coexists with `appAccountToken`, `quantity`, `promotionalOffer` and `winBackOfferId`
- a generic `PurchaseParam` sends no JWS

Native (`InAppPurchaseStoreKit2PluginTests.swift`):

- `testPurchaseWithIntroductoryOfferEligibilityJWS` drives a purchase with the option set and passes.

## Known limitation in local native verification

I could not run the full `RunnerTests` suite locally. It crashes in `StoreKit2TranslatorTests.setUp()` (`StoreKit2TranslatorTests.swift:69`) with `EXC_BREAKPOINT`/`SIGTRAP`, and the existing purchase-success tests fail with `PigeonError` plus an asynchronous timeout.

This is independent of this change: I reproduced the identical crash signature and the identical purchase-test failure on the unmodified upstream base (9f595f3) under the same Xcode 26.6 / iOS 26.5 simulator. It matches flutter/flutter#184678, including the same file and line and the same `SKTestSession` `SKInternalErrorDomain Code=3` symptom.

To be precise about what was verified locally: the patch-specific native test passes, and `dart format`, `flutter analyze`, the 106 Dart unit tests and `swift-format lint --strict` are all clean. I am **not** claiming the full native suite passes. Because product lookup fails in that environment, the native test exercises the call path but cannot by itself assert the option reached StoreKit; the deterministic assertions for that are the Dart tests above, and the Swift call site was additionally verified with `swiftc -typecheck` against the iOS 15 target.

No unrelated upstream test was modified or worked around.

## Pre-Review Checklist

[^1]: See "Known limitation in local native verification" above regarding the pre-existing native suite failure tracked in flutter/flutter#184678.
victorsanni pushed a commit to victorsanni/packages that referenced this pull request Sep 9, 2026
…WS (flutter#12584)

Adds an optional StoreKit 2-specific field to `Sk2PurchaseParam` that forwards a server-signed compact JWS to [`Product.PurchaseOption.introductoryOfferEligibility(compactJWS:)`](https://developer.apple.com/documentation/storekit/product/purchaseoption/introductoryoffereligibility(compactjws:)).

Fixes flutter/flutter#191668

## Problem

StoreKit 2 lets an app's own server decide, per purchase, whether an introductory offer applies. The app receives a compact JWS signed with an App Store Connect In-App Purchase key ([Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)) whose `allowIntroductoryOffer` claim Apple documents as "a Boolean value, `true` or `false`, that determines whether the customer is eligible for an introductory offer".

The plugin did not expose this purchase option, so apps whose eligibility rules live on their own server could not use the plugin's purchase path.

This is distinct from the existing `isIntroductoryOfferEligible(productId)`, which wraps `Product.SubscriptionInfo.isEligibleForIntroOffer` and **reads** the App Store's *default* eligibility. That read cannot express a server-signed per-purchase decision.

## Implementation

```dart
Sk2PurchaseParam(
  productDetails: productDetails,
  introductoryOfferEligibilityCompactJWS: compactJWSFromMyServer,
);
```

The value flows through `SK2ProductPurchaseOptions` and the Pigeon message to the Swift purchase-option builder:

```swift
if let compactJWS = options?.introductoryOfferEligibilityCompactJWS {
  purchaseOptions.insert(.introductoryOfferEligibility(compactJWS: compactJWS))
}
```

The JWS is opaque to the plugin: it is never generated, signed, parsed, validated or inspected.

Generated Pigeon files were regenerated with the repository's canonical `dart run pigeon --input pigeons/sk2_pigeon.dart`.

## Availability

No `#available` gate is required. `introductoryOfferEligibility(compactJWS:)` is declared `@backDeployed(before: iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4)` with availability iOS 15.0+ / macOS 12.0+, which is the same floor as the existing `@available(iOS 15.0, macOS 12.0, *)` StoreKit 2 extension it is added to.

## Backwards compatibility

Not breaking:

- the field is optional and null by default
- when null, no purchase option is added and the App Store applies its own default eligibility, so existing apps behave identically
- the new Pigeon field is appended, so existing encoded payloads decode unchanged
- StoreKit 1 is untouched
- `isIntroductoryOfferEligible`, promotional offers, win-back offers, `appAccountToken` and `quantity` are unchanged and continue to work alongside the new option

## Tests

Dart (`in_app_purchase_storekit_2_platform_test.dart`), all passing:

- null JWS -> no option is sent
- non-null JWS -> forwarded unchanged
- JWS coexists with `appAccountToken`, `quantity`, `promotionalOffer` and `winBackOfferId`
- a generic `PurchaseParam` sends no JWS

Native (`InAppPurchaseStoreKit2PluginTests.swift`):

- `testPurchaseWithIntroductoryOfferEligibilityJWS` drives a purchase with the option set and passes.

## Known limitation in local native verification

I could not run the full `RunnerTests` suite locally. It crashes in `StoreKit2TranslatorTests.setUp()` (`StoreKit2TranslatorTests.swift:69`) with `EXC_BREAKPOINT`/`SIGTRAP`, and the existing purchase-success tests fail with `PigeonError` plus an asynchronous timeout.

This is independent of this change: I reproduced the identical crash signature and the identical purchase-test failure on the unmodified upstream base (9f595f3) under the same Xcode 26.6 / iOS 26.5 simulator. It matches flutter/flutter#184678, including the same file and line and the same `SKTestSession` `SKInternalErrorDomain Code=3` symptom.

To be precise about what was verified locally: the patch-specific native test passes, and `dart format`, `flutter analyze`, the 106 Dart unit tests and `swift-format lint --strict` are all clean. I am **not** claiming the full native suite passes. Because product lookup fails in that environment, the native test exercises the call path but cannot by itself assert the option reached StoreKit; the deterministic assertions for that are the Dart tests above, and the Swift call site was additionally verified with `swiftc -typecheck` against the iOS 15 target.

No unrelated upstream test was modified or worked around.

## Pre-Review Checklist

[^1]: See "Known limitation in local native verification" above regarding the pre-existing native suite failure tracked in flutter/flutter#184678.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: in_app_purchase platform-ios platform-macos triage-ios Should be looked at in iOS triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[in_app_purchase_storekit] Expose StoreKit 2 introductoryOfferEligibility(compactJWS:) purchase option

4 participants