-
Notifications
You must be signed in to change notification settings - Fork 0
docs(concepts): dynamic offerings + monthly commitment references #24
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Dynamic Offerings — Runtime Plan / Offer Overrides | ||
|
|
||
| Applies to: **iOS, Android, React Native, Flutter, Cordova**. | ||
|
|
||
| A **dynamic offering** lets your app decide, **at runtime**, which plan (and optionally which promotional offer) a paywall slot resolves to — without republishing the Screen. In the Screen Composer, a plan-picker option (or a CTA) is bound to an **offering reference** (a string key). At runtime you map that reference to a concrete plan with `setDynamicOffering(...)`. | ||
|
kherembourg marked this conversation as resolved.
|
||
|
|
||
| Typical uses: contextual pricing (win-back vs onboarding), per-cohort plans, remote-config-driven experiments — all pointing the *same* screen at *different* plans. | ||
|
|
||
| ## The one rule that trips people up: offerings are applied **server-side, at fetch time** | ||
|
|
||
| The SDK does **not** rewrite the paywall locally. When it fetches a presentation (the placement call), it sends the currently-registered offerings to the Purchasely backend, and the backend substitutes the mapped plan/offer into the returned paywall JSON. | ||
|
|
||
| Consequences: | ||
|
|
||
| - **Register offerings BEFORE you fetch / display the placement.** An offering set *after* the paywall is fetched has no effect until the next fetch. | ||
| - Offerings are **persisted** across app launches until you remove them — a stale offering from a previous session still applies. Call `removeDynamicOffering` / `clearDynamicOfferings` when a context ends. | ||
| - Because substitution is server-side, the *reference* must exactly match what the Screen Composer expects, and the *plan vendor id* must exist in your catalog. | ||
|
|
||
| ## API by platform | ||
|
|
||
| `reference` + `planVendorId` are required; `offerVendorId` is optional (highlight a specific promotional offer). iOS additionally accepts `billingPlanType` — see [Monthly commitment billing](monthly-commitment.md). | ||
|
|
||
| ```swift | ||
| // iOS (Swift) — call before fetchPresentation / display | ||
| Purchasely.setDynamicOffering(reference: "onboarding_offer", | ||
| planVendorId: "PURCHASELY_PLUS_YEARLY", | ||
| offerVendorId: nil) { success in | ||
| // fetch / display the placement only after this returns true | ||
| } | ||
| Purchasely.getDynamicOfferings() // inspect | ||
| Purchasely.removeDynamicOffering(reference: "onboarding_offer") | ||
| Purchasely.clearDynamicOfferings() | ||
| ``` | ||
|
|
||
| ```kotlin | ||
| // Android (Kotlin) | ||
| Purchasely.setDynamicOffering( | ||
| reference = "onboarding_offer", | ||
| planVendorId = "PURCHASELY_PLUS_YEARLY", | ||
| offerVendorId = null, | ||
| ) { success -> /* then fetch / display */ } | ||
| ``` | ||
|
|
||
| ```ts | ||
| // React Native | ||
| const ok = await Purchasely.setDynamicOffering({ | ||
| reference: 'onboarding_offer', | ||
| planVendorId: 'PURCHASELY_PLUS_YEARLY', | ||
| offerVendorId: undefined, | ||
| }) | ||
| ``` | ||
|
|
||
| ```dart | ||
| // Flutter | ||
| final ok = await Purchasely.setDynamicOffering( | ||
| DynamicOffering(reference: 'onboarding_offer', planVendorId: 'PURCHASELY_PLUS_YEARLY'), | ||
| ); | ||
| ``` | ||
|
|
||
| `getDynamicOfferings`, `removeDynamicOffering`, and `clearDynamicOfferings` exist on every platform. | ||
|
|
||
| > **Note:** the `billingPlanType` argument is **iOS-only** at this time. Android, React Native, Flutter, and Cordova take `reference` / `planVendorId` / `offerVendorId` only. | ||
|
|
||
| ## Pitfall: one plan → one billing type per presentation | ||
|
|
||
| Do **not** register several offering references that all resolve to the **same plan** within a single presentation while carrying **different billing plan types** (for example one `.monthly` and one `.upFront` on the same plan). | ||
|
|
||
| When you do, the returned paywall ends up describing that same plan **more than once with conflicting billing types**. The SDK matches a plan by its vendor id, so it can no longer tell which billing type applies to it, and may report **`.unspecified`** to the purchase interceptor (and use it for the purchase) even though one of the offerings requested `.monthly`. Symptom: `parameters.billingPlanType == .unspecified` in the interceptor instead of the `.monthly` you set on the offering. | ||
|
|
||
| Guidance: | ||
|
|
||
| - Map a given plan to **a single billing plan type per presentation**. | ||
| - If you need both an up-front and a monthly-commitment variant on screen, back them with **two distinct plans / products**, not two offering references on the same plan. | ||
| - When re-configuring, `clearDynamicOfferings()` first so a leftover offering from a previous screen/session doesn't add a second mapping for the same plan. | ||
|
|
||
| See [Monthly commitment billing](monthly-commitment.md) for the billing-type feature itself and [common issues](../troubleshooting/common-issues.md) for the diagnostic entry. | ||
|
|
||
| ## Related | ||
|
|
||
| - [Monthly commitment billing (iOS)](monthly-commitment.md) — the `billingPlanType` feature | ||
| - [Promotional offers](promotional-offers.md) — what `offerVendorId` points at | ||
| - [Paywall actions](paywall-actions.md) — how a picker/CTA turns into a purchase action | ||
| - [Presentation cache](presentation-cache.md) — why "register before fetch" matters with preloading | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # Monthly Commitment Billing (Apple Advance Commitment) — iOS | ||
|
|
||
| Applies to: **iOS only** (native iOS SDK). Requires **iOS 26.4+** and **SDK v6+**. | ||
|
|
||
| Apple's **advance commitment** subscriptions let a long commitment period (e.g. a **12-month** commitment) be **billed monthly** instead of charged once up front. StoreKit models this as a *billing plan type* on the subscription: **up-front** vs **monthly**. The user commits to 12 months but is charged month by month. | ||
|
|
||
| Purchasely surfaces this as the public enum **`PLYBillingPlanType`**: | ||
|
|
||
| | Case | Meaning | | ||
| |------|---------| | ||
| | `.monthly` | 12-month commitment **billed monthly** (installment-style) | | ||
| | `.upFront` | Commitment **charged once up front** | | ||
| | `.unspecified` | No commitment billing info for this plan (plain subscription, or not configured) | | ||
|
|
||
| `.unspecified` is a deliberate third state — it is **not** the same as `.upFront`. Only an explicitly configured up-front commitment resolves to `.upFront`. | ||
|
|
||
| ## Eligibility — all of these must hold | ||
|
|
||
| - **iOS 26.4 or later** on the device. The StoreKit billing-plan-type API and the monthly-commitment purchase option exist only from 26.4; on older iOS the feature is unavailable. | ||
| - **Purchasely iOS SDK v6+**. | ||
| - **Storefront**: monthly commitment is offered in most App Store countries but **not** in the **United States** and **Singapore** (as of Apple's current rollout). On a US or Singapore storefront the SDK automatically **falls back from `.monthly` to `.upFront`** — a US/SG tester seeing `.upFront` is expected behavior, not a bug. | ||
| - **App Store Connect**: the product must be configured with advance-commitment (monthly) pricing. | ||
| - **Purchasely configuration**: the plan must carry the monthly billing plan type — set on the plan in the Screen Composer, or supplied at runtime via a [dynamic offering](dynamic-offerings.md) (`billingPlanType: .monthly`, iOS-only). | ||
|
|
||
| ## Setup checklist | ||
|
|
||
| 1. **App Store Connect** — enable advance commitment / monthly billing on the (yearly) product. | ||
| 2. **Purchasely Console** — set the plan's commitment billing type to monthly on the paywall, or pass it via `setDynamicOffering(..., billingPlanType: .monthly)`. | ||
| 3. **App** — Purchasely iOS SDK v6+, test on a real device or simulator running **iOS 26.4+**, signed into a **non-US / non-Singapore** App Store account (sandbox or TestFlight). | ||
|
|
||
| ## How it surfaces at runtime | ||
|
|
||
| - In the **purchase action interceptor**, `parameters.billingPlanType` reflects the resolved type for the tapped plan. | ||
| - In **Full** running mode, the SDK passes the correct StoreKit purchase option automatically when the resolved type is `.monthly`. | ||
| - In **Observer** mode, your own purchase code reads `parameters.billingPlanType` to decide how to purchase — so an incorrect value here changes what your app buys. | ||
|
|
||
| ## When you expect `.monthly` but get `.unspecified` | ||
|
|
||
| Check, in order: | ||
|
|
||
| 1. **Storefront** is not US / Singapore (those fall back to `.upFront`, and if the plan isn't set up as up-front either, you'll see `.unspecified`). | ||
| 2. **iOS version** is 26.4+. | ||
| 3. You did **not** map the **same plan to multiple offering references with different billing types** in one presentation — that ambiguity resolves to `.unspecified`. See the pitfall in [Dynamic offerings](dynamic-offerings.md#pitfall-one-plan--one-billing-type-per-presentation). | ||
| 4. The plan actually carries the monthly commitment type in the Console / dynamic offering. | ||
|
Copilot marked this conversation as resolved.
|
||
|
|
||
| ## Related | ||
|
|
||
| - [Dynamic offerings](dynamic-offerings.md) — how the `billingPlanType` is supplied at runtime, and the same-plan pitfall | ||
| - [Common issues](../troubleshooting/common-issues.md) — diagnostic entry for wrong / missing billing type | ||
| - [Programmatic purchases](programmatic-purchases.md) — app-side purchase APIs (Observer mode) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.