Skip to content

Commit 5190260

Browse files
authored
Update monetization guide with modern purchase flows (#4066)
1 parent be36581 commit 5190260

File tree

1 file changed

+31
-16
lines changed

1 file changed

+31
-16
lines changed

docs/developer-guide/Monetization.asciidoc

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,6 @@ public class HelloWorldIAP implements PurchaseCallback {
6464
...
6565
}
6666
67-
@Override
68-
public void itemRefunded(String sku) {
69-
...
70-
}
71-
72-
@Override
73-
public void subscriptionStarted(String sku) {
74-
...
75-
}
76-
77-
@Override
78-
public void subscriptionCanceled(String sku) {
79-
...
80-
}
81-
8267
@Override
8368
public void paymentFailed(String paymentCode, String failureReason) {
8469
...
@@ -92,7 +77,7 @@ public class HelloWorldIAP implements PurchaseCallback {
9277
}
9378
----
9479

95-
Using these callbacks, we'll be notified whenever something changes in our purchases. For our simple app we're only interested in `itemPurchased()` and `itemPurchaseError()`.
80+
Using these callbacks, we'll be notified whenever something changes in our purchases. For our simple app we're only interested in `itemPurchased()` and `itemPurchaseError()`. The legacy `itemRefunded()`, `subscriptionStarted()`, and `subscriptionCanceled()` hooks have been deprecated in the core API and are no longer dispatched by the stores. Instead of relying on callbacks for long-term entitlement state, query the current receipts at runtime using helpers such as `Purchase.wasPurchased(...)`, `Purchase.isSubscribed(...)`, or by iterating through `Purchase.getReceipts()` so that your UI always reflects the latest data provided by the underlying store.
9681

9782
Now in the start method, we'll add a button that allows the user to buy the world:
9883

@@ -222,6 +207,36 @@ NOTE: The concept of an "Non-renewable" subscription is unique to iTunes. Googl
222207

223208
IMPORTANT: The `Purchase` class includes both a `purchase()` method and a `subscribe()` method. On some platforms it makes no difference which one you use, but on Android it matters. If the product is set up as a subscription in Google Play, then you *must* use `subscribe()` to purchase the product. If it is set up as a regular product, then you *must* use `purchase()`. Since we enter "Non-renewable" subscriptions as regular products in the play store, we would use the `purchase()` method.
224209

210+
==== Promotional offers (iOS)
211+
212+
Apple allows you to present discounted introductory pricing to existing subscribers via https://developer.apple.com/documentation/storekit/skpaymentdiscount[promotional offers]. Codename One surfaces this capability through overloads of both `Purchase.purchase(String, PromotionalOffer)` and `Purchase.subscribe(String, PromotionalOffer)`, which forward the promotional context to StoreKit when you initiate the transaction. Promotional offers are only honoured by iOS, so the overloads simply fall back to the regular purchase flow on other platforms.
213+
214+
To build the signed discount payload required by Apple you can use the `ApplePromotionalOffer` helper:
215+
216+
[source,java]
217+
----
218+
ApplePromotionalOffer offer = new ApplePromotionalOffer();
219+
offer.setOfferIdentifier("my-intro-offer");
220+
offer.setKeyIdentifier("A1B2C3D4");
221+
offer.setNonce(UUID.randomUUID().toString());
222+
offer.setSignature(signatureFromYourServer);
223+
offer.setTimestamp(timestampFromYourServer);
224+
225+
Purchase purchase = Purchase.getInAppPurchase();
226+
purchase.subscribe(SKU_WORLD_MONTHLY, offer);
227+
----
228+
229+
Apple generates the signature and timestamp from your App Store Connect server notifications endpoint; Codename One simply passes them to the native StoreKit APIs. For one-time products you can call `purchase(sku, offer)` instead of `subscribe(...)`.
230+
231+
==== Restoring purchases and managing subscriptions
232+
233+
Both Apple and Google provide built-in user interfaces for restoring past purchases and managing subscription billing preferences. Codename One exposes these entry points so you can surface the native flows without reimplementing them yourself.
234+
235+
* Restores: Call `Purchase.isRestoreSupported()` before presenting a "Restore purchases" button. When supported (iOS currently implements this natively), invoke `Purchase.restore()` to prompt the operating system to re-deliver past transactions. Your app should implement `RestoreCallback` (similar to how you implement `PurchaseCallback`) so you can respond to individual `itemRestored(...)` events and to the completion or failure of the restore request.
236+
* Subscription management: Use `Purchase.isManageSubscriptionsSupported()` to detect whether the platform can show the subscription management UI. When it returns `true`, calling `Purchase.manageSubscriptions(null)` opens the store-specific settings screen (Apple's subscription center on iOS and Google Play's subscription management activity on Android). On Android you can optionally pass a SKU to deep-link directly to the plan the user should manage.
237+
238+
Because these flows are handled by the underlying store your UI doesn't need to rebuild any billing screens. Simply gate the buttons on the capability checks above so that iOS and Android users get the familiar restore/manage dialogs while other platforms can fall back to your own help copy.
239+
225240
==== The Server-Side
226241

227242
Since a subscription purchased on one user device *needs* to be available across the user's devices (Apple's rules for non-renewable subscriptions), our app will need to have a server-component. In this section, we'll gloss over that & "mock" the server interface. We'll go into the specifics of the server-side below.

0 commit comments

Comments
 (0)