Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5a962a2
Developer guide: settle the seven Monetization sentences that promise…
shai-almog Sep 13, 2026
1824002
Developer guide: three review fixes on the Monetization listings
shai-almog Sep 13, 2026
499df39
Developer guide: show the cached expiry before the user asks for a sync
shai-almog Sep 13, 2026
f8c8ce0
Developer guide: a failed sync must not cost the user the status they…
shai-almog Sep 13, 2026
5cce9e5
Developer guide: the startup sync has to reach the same label
shai-almog Sep 13, 2026
bb20935
Developer guide: one Synchronize Receipts button, not two
shai-almog Sep 13, 2026
99bc5b2
Developer guide: the two listings take the form rather than assuming one
shai-almog Sep 13, 2026
c586ed8
Developer guide: something has to call the two helpers
shai-almog Sep 13, 2026
ca3a585
Developer guide: declare the SKUs the whole chapter reads
shai-almog Sep 13, 2026
7060a2d
Developer guide: the subscription start() needs the same resume guard
shai-almog Sep 13, 2026
a4d7c32
Developer guide: the resume needs the synchronization more than the l…
shai-almog Sep 13, 2026
0310641
Developer guide: repaint the expiry label when the purchase lands
shai-almog Sep 13, 2026
0940e44
Developer guide: synchronize the Purchase instance the label reads
shai-almog Sep 13, 2026
fb043b0
Developer guide: the rental buttons read the shared Purchase too
shai-almog Sep 13, 2026
d51458e
Developer guide: hold no Purchase instance at all
shai-almog Sep 13, 2026
b56b533
Developer guide: the purchase callback can run before the form exists
shai-almog Sep 13, 2026
935a3df
Purchase: drain receipts queued while a synchronization was fetching
shai-almog Sep 13, 2026
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
19 changes: 19 additions & 0 deletions CodenameOne/src/com/codename1/payment/Purchase.java
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,25 @@ public void run() {
return;
}
syncInProgress = false;
// A purchase can complete while this fetch is in flight. Its
// postReceipt queued the receipt but could not start a
// synchronization of its own -- syncInProgress was true -- and this
// fetch was requested before the receipt existed, so the snapshot it
// brought back does not contain it. Reporting success now hands every
// waiting caller a result that predates the purchase, and the receipt
// stays pending until something synchronizes again. Run once more
// instead; the callbacks are still registered and fire from that pass,
// the same way onSubmitReceiptComplete continues draining.
//
// Guarded on receiptStore: with no store, synchronizeReceipts skips
// the submit branch and comes straight back here with the queue still
// non-empty, which would never terminate.
if (Boolean.TRUE.equals(fetchSucceeded)
&& receiptStore != null
&& !getPendingPurchases().isEmpty()) {
synchronizeReceipts(0, null);
return;
}
fireSynchronizeReceiptsCallbacks(Boolean.TRUE.equals(fetchSucceeded));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,54 @@ class MonetizationJava037Snippet {
Container myForm;
Component component;
Button button;
SpanLabel rentalStatus = new SpanLabel();

Form current;

void showRentalStatus() {
}

void addExpiryLabel(Form hi) {
}

void addSyncButton(Form hi) {
}
MultiButton myMultiButton;
Label label;
BrowserComponent browserComponent;
Resources theme;

// tag::monetization-java-037[]
public void start() {
if (current != null) {
// A resume. The form and everything on it survived, so rebuilding it
// would hand a second form components the first one still owns, which
// Container rejects.
current.show();
} else {
Form hi = new Form("Hello World", BoxLayout.y());

// ... the rest of the form

// ...
// The expiry label and the button that refreshes it, both of which
// the next two listings build.
addExpiryLabel(hi);
addSyncButton(hi);

// Now synchronize the receipts
iap.synchronizeReceipts(0, res->{
// Update the UI as necessary to reflect
current = hi;
hi.show();
}

// Outside the branch on purpose: a subscription can be bought, renewed
// or cancelled on another device while this one is suspended, so the
// resume needs this as much as the launch does.
Purchase.getInAppPurchase().synchronizeReceipts(0, success -> {
// Whatever this brought back, the expiry label is now out of date.
// Repaint it from the same method the manual button uses.
if (success) {
showRentalStatus();
current.revalidate();
}
});
}
// end::monetization-java-037[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,14 @@ class MonetizationJava040Snippet {

void snippet() throws Exception {
// tag::monetization-java-040[]
Purchase iap = Purchase.getInAppPurchase();
// A fresh Purchase on every click, never one captured by the
// listener: receipts are cached per instance, so a captured one keeps
// answering from before the purchase the user just made.
//...
Button rentWorld1M = new Button("Rent World 1 Month");
rentWorld1M.addActionListener(e->{
String msg = null;
Purchase iap = Purchase.getInAppPurchase();
if (iap.isSubscribed(PRODUCTS)) { // <1>
msg = "you're already renting the world until "
+iap.getExpiryDate(PRODUCTS) // <2>
Expand All @@ -98,7 +101,7 @@ void snippet() throws Exception {
msg = "Rent the world for 1 month?";
}
if (Dialog.show("Confirm", msg, "Yes", "No")) {
Purchase.getInAppPurchase().purchase(SKU_WORLD_1_MONTH); // <3>
iap.purchase(SKU_WORLD_1_MONTH); // <3>
// Note: since this is a non-renewable subscription it's a regular
// product in the play store - therefore you use the purchase() method.
// If it were a "subscription" product in the play store, then you
Expand All @@ -109,6 +112,7 @@ void snippet() throws Exception {
Button rentWorld1Y = new Button("Rent World 1 Year");
rentWorld1Y.addActionListener(e->{
String msg = null;
Purchase iap = Purchase.getInAppPurchase();
if (iap.isSubscribed(PRODUCTS)) {
msg = "you're already renting the world until "+
iap.getExpiryDate(PRODUCTS)+
Expand All @@ -117,16 +121,18 @@ void snippet() throws Exception {
msg = "Rent the world for 1 year?";
}
if (Dialog.show("Confirm", msg, "Yes", "No")) {
Purchase.getInAppPurchase().purchase(SKU_WORLD_1_YEAR);
iap.purchase(SKU_WORLD_1_YEAR);
// Note: since this is a non-renewable subscription it's a regular
// product in the play store - therefore you use the purchase() method.
// If it were a "subscription" product in the play store, then you
// would use subscribe() instead.
}
});
// end::monetization-java-040[]

}

Purchase iap = Purchase.getInAppPurchase();
static final String SKU_WORLD_1_YEAR = "com.example.world.year";
String[] PRODUCTS = {SKU_WORLD_1_MONTH, SKU_WORLD_1_YEAR};
static final String SKU_WORLD_1_MONTH = "com.example.world.month";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ abstract class Sample implements PurchaseCallback {
// tag::monetization-java-041[]
@Override
public void itemPurchased(String sku) {
// Nothing reads receipts off this instance until after the call below,
// so its cache loads from storage once the synchronization has written
// there -- which is the point of not holding a Purchase around.
Purchase iap = Purchase.getInAppPurchase();

// Reload the receipts from the store. This answers false when the receipt
Expand All @@ -98,13 +101,32 @@ public void itemPurchased(String sku) {
return;
}
ToastBar.showMessage("Your subscription has been extended to "+iap.getExpiryDate(PRODUCTS), FontImage.MATERIAL_THUMB_UP);

// The form the user is looking at still shows the status from before the
// purchase. The toast is not a substitute for repainting it.
//
// iOS registers its StoreKit observer during initialization, so an
// unfinished transaction can be re-delivered here before start() has
// built the form. The label is a field and is safe to set; the form may
// not exist yet, and start() paints it from the same method anyway.
showRentalStatus();
Comment thread
shai-almog marked this conversation as resolved.
Comment thread
shai-almog marked this conversation as resolved.
Comment thread
shai-almog marked this conversation as resolved.
if (current != null) {
current.revalidate();
Comment thread
shai-almog marked this conversation as resolved.
}
}

@Override
public void itemPurchaseError(String sku, String errorMessage) {
ToastBar.showErrorMessage("Failure occurred: "+errorMessage);
}
// end::monetization-java-041[]

Purchase iap = Purchase.getInAppPurchase();

Form current;

void showRentalStatus() {
}
}

String SKU_WORLD_1_MONTH = "com.example.world.month";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/

package com.codenameone.developerguide.snippets.generated;

import com.codename1.gpu.*;
import com.codename1.ui.*;
import com.codename1.ui.animations.*;
import com.codename1.ui.events.*;
import com.codename1.ui.geom.*;
import com.codename1.ui.layouts.*;
import com.codename1.ui.list.*;
import com.codename1.ui.plaf.*;
import com.codename1.ui.util.*;
import com.codename1.components.*;
import com.codename1.charts.models.*;
import com.codename1.charts.renderers.*;
import com.codename1.charts.views.*;
import com.codename1.capture.*;
import com.codename1.io.*;
import com.codename1.l10n.*;
import com.codename1.location.*;
import com.codename1.maps.*;
import com.codename1.media.*;
import com.codename1.messaging.*;
import com.codename1.payment.*;
import com.codename1.processing.*;
import com.codename1.properties.*;
import com.codename1.push.*;
import com.codename1.security.*;
import com.codename1.social.*;
import com.codename1.ui.spinner.*;
import java.io.*;
import java.util.*;


class MonetizationJava101Snippet {

Form hi;
Purchase iap = Purchase.getInAppPurchase();
String[] PRODUCTS = {"com.codename1.world.month", "com.codename1.world.year"};
SpanLabel rentalStatus = new SpanLabel();

void showRentalStatus() {
}

// tag::monetization-java-101[]
void addSyncButton(Form hi) {
Button syncReceipts = new Button("Synchronize Receipts");

syncReceipts.addActionListener(e -> {
Purchase.getInAppPurchase().synchronizeReceipts(0, success -> {
// synchronizeReceipts reports true only when every pending
// purchase reached the receipt store AND the receipts came
// back. On false nothing was reloaded, so there is nothing
// new to show and the status on screen stays as it was.
if (success) {
showRentalStatus();
hi.revalidate();
} else {
ToastBar.showErrorMessage("Could not reach the receipt store");
}
});
});

hi.add(syncReceipts);
}
// end::monetization-java-101[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/

package com.codenameone.developerguide.snippets.generated;

import com.codename1.gpu.*;
import com.codename1.ui.*;
import com.codename1.ui.animations.*;
import com.codename1.ui.events.*;
import com.codename1.ui.geom.*;
import com.codename1.ui.layouts.*;
import com.codename1.ui.list.*;
import com.codename1.ui.plaf.*;
import com.codename1.ui.util.*;
import com.codename1.components.*;
import com.codename1.charts.models.*;
import com.codename1.charts.renderers.*;
import com.codename1.charts.views.*;
import com.codename1.capture.*;
import com.codename1.io.*;
import com.codename1.l10n.*;
import com.codename1.location.*;
import com.codename1.maps.*;
import com.codename1.media.*;
import com.codename1.messaging.*;
import com.codename1.payment.*;
import com.codename1.processing.*;
import com.codename1.properties.*;
import com.codename1.push.*;
import com.codename1.security.*;
import com.codename1.social.*;
import com.codename1.ui.spinner.*;
import java.io.*;
import java.util.*;


class MonetizationJava102Snippet {

Form hi;
Purchase iap = Purchase.getInAppPurchase();
String[] PRODUCTS = {"com.codename1.world.month", "com.codename1.world.year"};

// tag::monetization-java-102[]
// A field, not a local: the synchronization at the end of start() and the
// button above both have to reach this label.
SpanLabel rentalStatus = new SpanLabel();

void addExpiryLabel(Form hi) {
Comment thread
shai-almog marked this conversation as resolved.
// The receipts already on the device answer this with no round trip,
// so the label is right the moment the form appears rather than after
// the first synchronization comes back.
showRentalStatus();
hi.add(rentalStatus);
Comment thread
shai-almog marked this conversation as resolved.
Comment thread
shai-almog marked this conversation as resolved.
}

void showRentalStatus() {
Comment thread
shai-almog marked this conversation as resolved.
Purchase iap = Purchase.getInAppPurchase();
if (iap.isSubscribed(PRODUCTS)) {
rentalStatus.setText("World rental expires " + iap.getExpiryDate(PRODUCTS));
Comment thread
shai-almog marked this conversation as resolved.
} else {
rentalStatus.setText("You do not currently have a subscription to the world");
}
}
// end::monetization-java-102[]
}
Loading
Loading