All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.codename1.payment.Purchase Maven / Gradle / Ivy

There is a newer version: 7.0.164
Show newest version
/*
 * 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.codename1.payment;

import com.codename1.io.Log;
import com.codename1.io.Storage;
import com.codename1.io.Util;
import com.codename1.ui.Dialog;
import com.codename1.ui.Display;
import com.codename1.util.SuccessCallback;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * Represents the status of in-app-purchase goods, this class provides information
 * about products purchased by a user as well as the ability to purchase additional
 * products. There are generally two types of payment systems: Manual and managed.
 * In manual payments we pay a specific amount in a specific currency  while with managed
 * payment systems we work against a product catalog defined in the server.
 * 

In-app-purchase API's rely on managed server based products, other payment systems * use the manual approach. An application dealing with virtual goods must support both * since not all devices feature in-app-purchase API's. An application dealing with physical * goods & services must use the latter according to the TOS of current in-app-purchase * solutions. * * @author Shai Almog */ public abstract class Purchase { private static ReceiptStore receiptStore; private static final String RECEIPTS_KEY="CN1SubscriptionsData.dat"; private static final String RECEIPTS_REFRESH_TIME_KEY="CN1SubscriptionsDataRefreshTime.dat"; private static final String PENDING_PURCHASE_KEY = "PendingPurchases.dat"; private static List receipts; private static Date receiptsRefreshTime; /** * Installs a given receipt store to handle receipt management * @param store */ public final void setReceiptStore(ReceiptStore store) { receiptStore = store; } protected final ReceiptStore getReceiptStore() { return receiptStore; } /** * Gets all of the receipts for this app. Note: You should periodically * reload the receipts from the server to make sure that the user * hasn't canceled a receipt or renewed one. * @return List of receipts for purchases this app. */ public final List getReceipts() { synchronized (RECEIPTS_KEY) { if (receipts == null) { if (Storage.getInstance().exists(RECEIPTS_KEY)) { Receipt.registerExternalizable(); try { receipts = (List)Storage.getInstance().readObject(RECEIPTS_KEY); } catch (Exception ex) { Log.p("Failed to load receipts from "+RECEIPTS_KEY); Log.e(ex); receipts = new ArrayList(); } } else { receipts = new ArrayList(); } } return receipts; } } /** * Gets all of the receipts for the specified skus. * @param skus The skus for which to get receipts. * @return All receipts for the given skus. */ public final Receipt[] getReceipts(String... skus) { List out = new ArrayList(); List lSkus = Arrays.asList(skus); for (Receipt r : getReceipts()) { if (lSkus.contains(r.getSku())) { out.add(r); } } return out.toArray(new Receipt[out.size()]); } /** * Gets the time that receipts were last refreshed. * @return */ private Date getReceiptsRefreshTime() { synchronized(RECEIPTS_KEY){ if(receiptsRefreshTime == null) { if (Storage.getInstance().exists(RECEIPTS_REFRESH_TIME_KEY)) { receiptsRefreshTime = (Date)Storage.getInstance().readObject(RECEIPTS_REFRESH_TIME_KEY); } else { return new Date(-1l); } } return receiptsRefreshTime; } } /** * Sets the list of receipts. * @param data */ private void setReceipts(List data) { synchronized(RECEIPTS_KEY) { receipts = new ArrayList(); receipts.addAll(data); Storage.getInstance().writeObject(RECEIPTS_KEY, receipts); } } /** * Updates the last refresh time for receipts. * @param time */ private void setReceiptsRefreshTime(Date time) { synchronized(RECEIPTS_KEY) { receiptsRefreshTime = time; Storage.getInstance().writeObject(RECEIPTS_REFRESH_TIME_KEY, receiptsRefreshTime); } } /** * Indicates whether the purchasing platform supports manual payments which * are just payments of a specific amount of money. * * @return true if manual payments are supported */ public boolean isManualPaymentSupported() { return false; } /** * Indicates whether the purchasing platform supports managed payments which * work by picking products that are handled by the servers/OS of the platform vendor. * * @return true if managed payments are supported */ public boolean isManagedPaymentSupported() { return false; } /** * Performs payment of a specific amount based on the manual payment API, notice that * this doesn't use the in-app-purchase functionality of the device! * * @param amount the amount to pay * @param currency the three letter currency type * @return a token representing the pending transaction which will be matched * when receiving a callback from the platform or a null if the payment has * failed or was canceled * @throws RuntimeException This method is a part of the manual payments API and will fail if * isManualPaymentSupported() returns false */ public String pay(double amount, String currency) { throw new RuntimeException("Unsupported"); } /** * Performs payment of a specific amount based on the manual payment API, notice that * this doesn't use the in-app-purchase functionality of the device! * * @param amount the amount to pay * @param currency the three letter currency type * @param invoiceNumber application specific invoice number * @return a token representing the pending transaction which will be matched * when receiving a callback from the platform or a null if the payment has * failed or was canceled * @throws RuntimeException This method is a part of the manual payments API and will fail if * isManualPaymentSupported() returns false */ public String pay(double amount, String currency, String invoiceNumber) { return pay(amount, currency); } /** * Indicates whether the payment platform supports things such as "item listing" or * requires that items be coded into the system. iOS provides listing and pricing * where Android expects developers to redirect into the Play application for * application details. * @return true if the OS supports this behavior */ public boolean isItemListingSupported() { return false; } /** * Returns the product list for the given SKU array * * @param sku the ids for the specific products * @return the product instances * @throws RuntimeException This method is a part of the managed payments API and will fail if * isManagedPaymentSupported() returns false * @throws RuntimeException This method works only if isItemListingSupported() returns true */ public Product[] getProducts(String[] skus) { throw new RuntimeException("Unsupported"); } /** * Returns true if the given SKU was purchased in the past, notice this method might not * work as expected for Unmanaged/consumable products which can be purchased multiple * times. In addition, this will only return true if the product was purchased (or * has been restored) on the current device. * * @param sku the id of the product * @return true if the product was purchased * @throws RuntimeException This method is a part of the managed payments API and will fail if * isManagedPaymentSupported() returns false */ public boolean wasPurchased(String sku) { throw new RuntimeException("Unsupported"); } /** * Begins the purchase process for the given SKU * *

On Android you *must* use {@link #subscribe(java.lang.String) } for play store subscription products instead of this method. You cannot use {@link #purchase(java.lang.String) }. On iOS * there is no difference between {@link #subscribe(java.lang.String) } and {@link #purchase(java.lang.String) }, so if you are simulating subscriptions * on iOS using auto-renewables, you are better to use {@link #subscribe(java.lang.String) } as this will work correctly on both Android * and iOS.

* * @param sku the SKU with which to perform the purchase process * @throws RuntimeException This method is a part of the managed payments API and will fail if * isManagedPaymentSupported() returns false */ public void purchase(String sku) { throw new RuntimeException("Unsupported"); } /** * Begins the purchase process for the given SKU using a provided promotional offer. * *

Promotional offers are currently only supported on iOS. See Apple's documentation

* * @param sku the SKU with which to perform the purchase process * @param promotionalOffer The promotional offer. * @throws RuntimeException This method is a part of the managed payments API and will fail if * isManagedPaymentSupported() returns false * @see ApplePromotionalOffer */ public void purchase(String sku, PromotionalOffer promotionalOffer) { throw new RuntimeException("Unsupported"); } /** * Begins subscribe process for the given subscription SKU * *

On Android you *must* use this method for play store subscription products. You cannot use {@link #purchase(java.lang.String) }. On iOS * there is no difference between {@link #subscribe(java.lang.String) } and {@link #purchase(java.lang.String) }, so if you are simulating subscriptions * on iOS using auto-renewables, you are better to use {@link #subscribe(java.lang.String) } as this will work correctly on both Android * and iOS.

* * @param sku the SKU with which to perform the purchase process * @throws RuntimeException This method is a part of the managed payments API and will fail if * isManagedPaymentSupported() returns false */ public void subscribe(String sku) { if (receiptStore != null) { purchase(sku); return; } throw new RuntimeException("Unsupported"); } /** * Begins subscribe process for the given subscription SKU using a provided promotional offer. * *

Promotional offers are currently only supported on iOS. See Apple's documentation

* * @param sku the SKU with which to perform the purchase process * @param promotionalOffer The promotional offer. * @throws RuntimeException This method is a part of the managed payments API and will fail if * isManagedPaymentSupported() returns false * @see ApplePromotionalOffer */ public void subscribe(String sku, PromotionalOffer promotionalOffer) { if (receiptStore != null) { purchase(sku, promotionalOffer); return; } throw new RuntimeException("Unsupported"); } /** * Cancels the subscription to a given SKU * * @param sku the SKU with which to perform the purchase process * @throws RuntimeException This method is a part of the managed payments API and will fail if * isManagedPaymentSupported() returns false */ public void unsubscribe(String sku) { throw new RuntimeException("Unsupported"); } /** * Gets a list of purchases that haven't yet been sent to the server. You can * use this for diagnostic and debugging purposes periodically in the app to * make sure there aren't a queue of purchases that aren't getting submitted * to the server. * @return List of receipts that haven't been sent to the server. */ public List getPendingPurchases() { synchronized(PENDING_PURCHASE_KEY) { Storage s = Storage.getInstance(); Util.register(new Receipt()); if (s.exists(PENDING_PURCHASE_KEY)) { return (List)s.readObject(PENDING_PURCHASE_KEY); } else { return new ArrayList(); } } } /** * Adds a receipt to be pushed to the server. * @param receipt */ private void addPendingPurchase(Receipt receipt) { synchronized(PENDING_PURCHASE_KEY) { Storage s = Storage.getInstance(); List pendingPurchases = getPendingPurchases(); pendingPurchases.add(receipt); s.writeObject(PENDING_PURCHASE_KEY, pendingPurchases); } } /** * Removes a receipt from pending purchases. * @param transactionId * @return */ private Receipt removePendingPurchase(String transactionId) { synchronized(PENDING_PURCHASE_KEY) { Storage s = Storage.getInstance(); List pendingPurchases = getPendingPurchases(); Receipt found = null; for (Receipt r : pendingPurchases) { if (r.getTransactionId() != null && r.getTransactionId().equals(transactionId)) { found = r; break; } } if (found != null) { pendingPurchases.remove(found); s.writeObject(PENDING_PURCHASE_KEY, pendingPurchases); return found; } else { return null; } } } /** * Boolean flag to prevent {@link #synchronizeReceipts(long, com.codename1.util.SuccessCallback) } * re-entry. */ private static boolean syncInProgress; /** * Flag to prevent {@link #loadReceipts(long, com.codename1.util.SuccessCallback)} re-entry. */ private static boolean loadInProgress; public final void synchronizeReceipts() { if (syncInProgress) { return; } synchronizeReceipts(0, null); } private static final Object synchronizationLock = new Object(); private static List> synchronizeReceiptsCallbacks; private void fireSynchronizeReceiptsCallbacks(boolean result) { synchronized(synchronizationLock) { if (synchronizeReceiptsCallbacks == null) { return; } for (SuccessCallback cb : synchronizeReceiptsCallbacks) { cb.onSucess(result); } synchronizeReceiptsCallbacks.clear(); } } /** * Synchronize with receipt store. This will try to submit any pending purchases * to the receipt store, and then reload receipts from the receipt store * @param ifOlderThanMs Only fetch receipts if they haven't been fetched in {@code ifOlderThanMs} milliseconds. * @param callback Callback called when sync is done. Will be passed true if all pending purchases were successfully * submitted to the receipt store AND receipts were successfully loaded. */ public final void synchronizeReceipts(final long ifOlderThanMs, final SuccessCallback callback) { synchronized(synchronizationLock) { if (callback != null) { if (synchronizeReceiptsCallbacks == null) { synchronizeReceiptsCallbacks = new ArrayList>(); } synchronizeReceiptsCallbacks.add(callback); } if (syncInProgress) { return; } syncInProgress = true; } synchronized(PENDING_PURCHASE_KEY) { List pending = getPendingPurchases(); if (!pending.isEmpty() && receiptStore != null) { final Receipt receipt = pending.get(0); receiptStore.submitReceipt(pending.get(0), new SuccessCallback() { public void onSucess(Boolean submitSucceeded) { if (submitSucceeded) { removePendingPurchase(receipt.getTransactionId()); syncInProgress = false; // If the submit succeeded we need to refetch // so we set this to zero here. synchronizeReceipts(0, callback); } else { syncInProgress = false; fireSynchronizeReceiptsCallbacks(false); } } }); } else { loadReceipts(ifOlderThanMs, new SuccessCallback() { public void onSucess(Boolean fetchSucceeded) { syncInProgress = false; fireSynchronizeReceiptsCallbacks(fetchSucceeded); } }); } } } /** * Posts a receipt to be added to the receipt store. * @param r The receipt to post. */ private void postReceipt(Receipt r) { addPendingPurchase(r); Display.getInstance().callSerially(new Runnable() { public void run() { synchronizeReceipts(); } }); } /** * Posts a receipt to be added to the receipt store. * * @deprecated For internal implementation use only. * @param sku The sku of the product * @param transactionId The transaction ID * @param datePurchased The date of the purchase. */ public static void postReceipt(String storeCode, String sku, String transactionId, long datePurchased, String orderData) { Receipt r = new Receipt(); r.setSku(sku); r.setTransactionId(transactionId); r.setOrderData(orderData); r.setStoreCode(storeCode); if (datePurchased > 0) { r.setPurchaseDate(new Date(datePurchased)); } else { r.setPurchaseDate(new Date()); } Purchase.getInAppPurchase().postReceipt(r); } /** * Synchronize receipts and wait for the sync to complete before proceeding. * @param ifOlderThanMs Only re-fetch if it hasn't been reloaded in this number of milliseconds. * @return True if the synchronization succeeds. False otherwise. */ public final boolean synchronizeReceiptsSync(long ifOlderThanMs) { final boolean[] complete = new boolean[1]; final boolean[] success = new boolean[1]; synchronizeReceipts(ifOlderThanMs, new SuccessCallback() { public void onSucess(Boolean value) { complete[0] = true; success[0] = value; synchronized(complete) { complete.notifyAll(); } } }); if (!complete[0]) { Display.getInstance().invokeAndBlock(new Runnable() { public void run() { while (!complete[0]) { synchronized(complete) { try { complete.wait(); } catch (Exception ex) { } } } } }); } return success[0]; } /** * Fetches receipts from the IAP service so that we know we are dealing * with current data. This method should be called before checking a * subscription expiry date so that any changes the user has made in the * store is reflected here (e.g. cancelling or renewing subscription). * @param ifOlderThanMs Update is only performed if more than {@code ifOlderThanMs} milliseconds has elapsed * since the last successful fetch. * @param callback Callback called when request is complete. Passed {@code true} if * the data was successfully fetched. {@code false} otherwise. */ private final void loadReceipts(long ifOlderThanMs, final SuccessCallback callback) { if (loadInProgress) { Log.p("Did not load receipts because another load is in progress"); callback.onSucess(false); return; } loadInProgress = true; Date lastRefreshTime = getReceiptsRefreshTime(); Date now = new Date(); if (lastRefreshTime.getTime() + ifOlderThanMs > now.getTime()) { Log.p("Receipts were last refreshed at "+ lastRefreshTime + " so we won't refetch."); loadInProgress = false; callback.onSucess(true); return; } List oldData = new ArrayList(); oldData.addAll(getReceipts()); SuccessCallback onSuccess = new SuccessCallback() { public void onSucess(Receipt[] value) { if (value != null) { setReceipts(Arrays.asList(value)); setReceiptsRefreshTime(new Date()); loadInProgress = false; callback.onSucess(Boolean.TRUE); } else { loadInProgress = false; callback.onSucess(Boolean.FALSE); } } }; if (receiptStore != null) { receiptStore.fetchReceipts(onSuccess); } else { Log.p("No receipt store is currently registered so no receipts were fetched"); loadInProgress = false; callback.onSucess(Boolean.FALSE); } } /** * Gets the latest expiry date for a set of SKUs as reflected by a set of receipts. * @param receipts Receipts to check against. * @param skus The set of skus we are checking for. * @return The expiry date for a set of skus */ private Date getExpiryDate(Receipt[] receipts, String ... skus) { Date expiryDate = new Date(0l); List lSkus = Arrays.asList(skus); long now = System.currentTimeMillis(); for (Receipt r : receipts) { if (!lSkus.contains(r.getSku())) { continue; } if (r.getExpiryDate() == null) { continue; } if (r.getExpiryDate().getTime() > expiryDate.getTime() && r.getCancellationDate() == null) { expiryDate = r.getExpiryDate(); } } return expiryDate; } /** * Gets the expiry date for a set of skus. * @param skus The skus to check. The latest expiry date of the set will be used. * @return The expiry date for a set of skus. */ public final Date getExpiryDate(String... skus) { return getExpiryDate(getReceipts(skus), skus); } /** * Checks to see if the user is currently subscribed to any of the given skus. A user * is deemed to be subscribed if {@link #getExpiryDate(java.lang.String...)} returns a date * later than now. * @param skus Set of skus to check. * @return */ public final boolean isSubscribed(String... skus) { Date exp = getExpiryDate(skus); return exp != null && exp.getTime() >= System.currentTimeMillis(); } /** * Given the {@code publishDate} for an item, this returns the effective receipt that * relates to that item. This will either be a receipt with {@code purchaseDate <= publishDate <= expiryDate} or * the earliest receipt with {@code publishDate < purchaseDate}, or null if no receipts * @param receipts * @param publishDate * @param skus * @return */ private Receipt getFirstReceiptExpiringAfter(Receipt[] receipts, Date publishDate, String... skus) { List lSkus = Arrays.asList(skus); Receipt effectiveReceipt = null; for (Receipt r : receipts) { if (!lSkus.contains(r.getSku())) { continue; } if (r.getExpiryDate() == null) { continue; } if (r.getPurchaseDate() != null && r.getPurchaseDate().getTime() <= publishDate.getTime() && r.getExpiryDate().getTime() >= publishDate.getTime() && (r.getCancellationDate() == null || r.getCancellationDate().getTime() >= publishDate.getTime() )) { // Exact match in range. return r; } if (r.getPurchaseDate() != null && r.getPurchaseDate().getTime() <= publishDate.getTime()) { // The previous check would see if we had an exact match. // If we are here and the purchase date is before the issue date, // then the receipt had expired by the time this issue came out continue; } // At this point we know that the issue date is before the purchase date if (effectiveReceipt == null || effectiveReceipt.getPurchaseDate().getTime() > r.getPurchaseDate().getTime()) { effectiveReceipt = r; } } return effectiveReceipt; } /** * Gets the first receipt that expires after the specified date for the provided * skus. * @param dt * @param skus * @return */ public Receipt getFirstReceiptExpiringAfter(Date dt, String... skus) { return getFirstReceiptExpiringAfter(getReceipts(skus), dt, skus); } /** * Fetch receipts from IAP service synchronously. * @param ifOlderThanMs If the current data is not older than this number of milliseconds * then it will not attempt to fetch the receipts. * @return true if data was successfully retrieved. false otherwise. */ private boolean loadReceiptsSync(long ifOlderThanMs) { final boolean[] complete = new boolean[1]; final boolean[] success = new boolean[1]; loadReceipts(ifOlderThanMs, new SuccessCallback() { public void onSucess(Boolean value) { complete[0] = true; success[0] = value; synchronized(complete) { complete.notifyAll(); } } }); if (!complete[0]) { Display.getInstance().invokeAndBlock(new Runnable() { public void run() { while (!complete[0]) { synchronized(complete) { try { complete.wait(); } catch (Exception ex) { } } } } }); } return success[0]; } /** * Indicates whether refunding is possible when the SKU is purchased * @param sku the sku * @return true if the SKU can be refunded */ public boolean isRefundable(String sku) { return false; } /** * Tries to refund the given SKU if applicable in the current market/product * * @param sku the id for the product */ public void refund(String sku) { } /** * Returns the native OS purchase implementation if applicable, if unavailable this * method will try to fallback to a custom purchase implementation and failing that * will return null * * @return instance of the purchase class or null */ public static Purchase getInAppPurchase() { return Display.getInstance().getInAppPurchase(); } /** * @deprecated use the version that takes no arguments */ public static Purchase getInAppPurchase(boolean d) { return Display.getInstance().getInAppPurchase(); } /** * Returns true if the subscription API is supported in this platform * * @return true if the subscription API is supported in this platform */ public boolean isSubscriptionSupported() { return false; } /** * Some platforms support subscribing but don't support unsubscribe * * @return true if the subscription API allows for unsubscribe * @deprecated use {@link #isManageSubscriptionsSupported()} instead */ public boolean isUnsubscribeSupported() { return isSubscriptionSupported(); } /** * Indicates whether a purchase restore button is supported by the OS * @return true if you can invoke the restore method */ public boolean isRestoreSupported() { return false; } /** * Restores purchases if applicable, this will only work if isRestoreSupported() returns true */ public void restore() { } /** * Checks to see if this platform supports the {@link #manageSubscriptions(java.lang.String) } method. * @return True if the platform supports the {@link #manageSubscriptions(java.lang.String) } method. * * @since 6.0 */ public boolean isManageSubscriptionsSupported() { return false; } /** * Open the platform's UI for managing subscriptions. Currently iOS and Android * are the only platforms that support this. Other platforms will simply display a dialog stating that * it doesn't support this feature. Use the {@link #isManageSubscriptionsSupported() } method to check * if the platform supports this feature. * * * @param sku Optional sku of product whose subscription you wish to manage. If left {@literal null}, then * the general subscription management UI will be opened. iOS doesn't support "deep-linking" directly to the * management for a particular sku, so this parameter is ignored there. If included on Android, howerver, * it will open the UI for managing the specified sku. * * @since 6.0 */ public void manageSubscriptions(String sku) { Dialog.show("Not Supported", "This platform doesn't support in-app subscription management. ", "OK", null); } /** * Returns the store code associated with this in-app purchase object. * @return The store code. One of {@link Receipt#STORE_CODE_ITUNES}, {@link Receipt#STORE_CODE_PLAY}, {@link Receipt#STORE_CODE_SIMULATOR}, or * {@link Receipt#STORE_CODE_WINDOWS}. * @since 8.0 */ public String getStoreCode() { return null; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy