diff --git a/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToFreeTrialPeriodConverter.java b/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToFreeTrialPeriodConverter.java index 93fc780c..e95fc62f 100644 --- a/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToFreeTrialPeriodConverter.java +++ b/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToFreeTrialPeriodConverter.java @@ -1,7 +1,7 @@ package com.badlogic.gdx.pay.android.googlebilling; -import com.badlogic.gdx.pay.FreeTrialPeriod; -import com.badlogic.gdx.pay.FreeTrialPeriod.PeriodUnit; +import com.badlogic.gdx.pay.SubscriptionPeriod; +import com.badlogic.gdx.pay.SubscriptionPeriod.PeriodUnit; import javax.annotation.Nonnull; @@ -14,10 +14,10 @@ class Iso8601DurationStringToFreeTrialPeriodConverter { * the spec */ @Nonnull - public static FreeTrialPeriod convertToFreeTrialPeriod(@Nonnull String iso8601Duration) { + public static SubscriptionPeriod convertToFreeTrialPeriod(@Nonnull String iso8601Duration) { final int numberOfUnits = Integer.parseInt(iso8601Duration.substring(1, iso8601Duration.length() -1 )); final PeriodUnit unit = PeriodUnit.parse(iso8601Duration.substring(iso8601Duration.length() - 1).charAt(0)); - return new FreeTrialPeriod(numberOfUnits, unit); + return new SubscriptionPeriod(numberOfUnits, unit); } } diff --git a/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java b/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java index bf65da6f..78ea84d2 100644 --- a/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java +++ b/gdx-pay-android-googlebilling/src/com/badlogic/gdx/pay/android/googlebilling/PurchaseManagerGoogleBilling.java @@ -170,7 +170,8 @@ private Information convertSkuDetailsToInformation(SkuDetails skuDetails) { String priceString = skuDetails.getPrice(); return Information.newBuilder() .localName(skuDetails.getTitle()) - .freeTrialPeriod(convertToFreeTrialPeriod(skuDetails.getFreeTrialPeriod())) + .freeTrialPeriod(convertToSubscriptionPeriod(skuDetails.getFreeTrialPeriod())) + .subscriptionPeriod(convertToSubscriptionPeriod(skuDetails.getSubscriptionPeriod())) .localDescription(skuDetails.getDescription()) .localPricing(priceString) .priceCurrencyCode(skuDetails.getPriceCurrencyCode()) @@ -183,7 +184,7 @@ private Information convertSkuDetailsToInformation(SkuDetails skuDetails) { * @param iso8601Duration in ISO 8601 format. */ @Nullable - private FreeTrialPeriod convertToFreeTrialPeriod(@Nullable String iso8601Duration) { + private SubscriptionPeriod convertToSubscriptionPeriod(@Nullable String iso8601Duration) { if (iso8601Duration == null || iso8601Duration.isEmpty()) { return null; } @@ -293,6 +294,7 @@ private void handlePurchase(List purchases, boolean fromRestore) { transaction.setStoreName(PurchaseManagerConfig.STORE_NAME_ANDROID_GOOGLE); transaction.setPurchaseTime(new Date(purchase.getPurchaseTime())); transaction.setPurchaseText("Purchased: " + purchase.getSku()); + transaction.setInformation(getInformation(purchase.getSku())); transaction.setReversalTime(null); transaction.setReversalText(null); transaction.setTransactionData(purchase.getOriginalJson()); diff --git a/gdx-pay-android-googlebilling/test/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToFreeTrialPeriodConverterTest.java b/gdx-pay-android-googlebilling/test/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToSubscriptionPeriodConverterTest.java similarity index 55% rename from gdx-pay-android-googlebilling/test/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToFreeTrialPeriodConverterTest.java rename to gdx-pay-android-googlebilling/test/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToSubscriptionPeriodConverterTest.java index 2e745293..474a1753 100644 --- a/gdx-pay-android-googlebilling/test/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToFreeTrialPeriodConverterTest.java +++ b/gdx-pay-android-googlebilling/test/com/badlogic/gdx/pay/android/googlebilling/Iso8601DurationStringToSubscriptionPeriodConverterTest.java @@ -1,17 +1,17 @@ package com.badlogic.gdx.pay.android.googlebilling; -import com.badlogic.gdx.pay.FreeTrialPeriod; -import com.badlogic.gdx.pay.FreeTrialPeriod.PeriodUnit; +import com.badlogic.gdx.pay.SubscriptionPeriod; +import com.badlogic.gdx.pay.SubscriptionPeriod.PeriodUnit; import org.junit.Test; import static org.junit.Assert.*; -public class Iso8601DurationStringToFreeTrialPeriodConverterTest { +public class Iso8601DurationStringToSubscriptionPeriodConverterTest { @Test public void convertsStringWithFewDays() { - final FreeTrialPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P3D"); + final SubscriptionPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P3D"); assertEquals(3, period.getNumberOfUnits()); assertEquals(PeriodUnit.DAY, period.getUnit()); @@ -20,7 +20,7 @@ public void convertsStringWithFewDays() { @Test public void convertsStringWithMoreThenTenDays() { - final FreeTrialPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P14D"); + final SubscriptionPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P14D"); assertEquals(14, period.getNumberOfUnits()); assertEquals(PeriodUnit.DAY, period.getUnit()); @@ -29,7 +29,7 @@ public void convertsStringWithMoreThenTenDays() { @Test public void convertsStringWitSixMonths() { - final FreeTrialPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P6M"); + final SubscriptionPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P6M"); assertEquals(6, period.getNumberOfUnits()); assertEquals(PeriodUnit.MONTH, period.getUnit()); @@ -38,7 +38,7 @@ public void convertsStringWitSixMonths() { @Test public void convertsStringWithOneYear() { - final FreeTrialPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P1Y"); + final SubscriptionPeriod period = Iso8601DurationStringToFreeTrialPeriodConverter.convertToFreeTrialPeriod("P1Y"); assertEquals(1, period.getNumberOfUnits()); assertEquals(PeriodUnit.YEAR, period.getUnit()); diff --git a/gdx-pay-iosrobovm-apple/README.md b/gdx-pay-iosrobovm-apple/README.md index 5d21fffa..db67af13 100644 --- a/gdx-pay-iosrobovm-apple/README.md +++ b/gdx-pay-iosrobovm-apple/README.md @@ -19,5 +19,48 @@ Next to other ways, I find the easiest way to test the IAP the following: * your build installed from TestFlight will have working IAPs that are not charged to the users -(1) In order for you to use your actual IAP in your app you also have to fill some information regarding your App Store Connect account. Normally you will see a warning if something is missing, but sometimes when your app is marked as distributed for free, the warnings won't show. In case of IAP, make sure that you have filled required information in following section: -My Apps -> Agreements, Tax, and Banking -> Paid Apps. It's status should be "Active". As stated there: "The Paid Apps agreement alllows your organization to sell apps on the App Store or **offer in-app purchases.**" \ No newline at end of file +(1) In order for you to use your actual IAP in your app you also have to fill some information regarding your App Store +Connect account. Normally you will see a warning if something is missing, but sometimes when your app is marked as +distributed for free, the warnings won't show. In case of IAP, make sure that you have filled required information in +following section: My Apps -> Agreements, Tax, and Banking -> Paid Apps. It's status should be "Active". As stated +there: "The Paid Apps agreement alllows your organization to sell apps on the App Store or **offer in-app purchases.**" + + +## Subscriptions + +To verify if the user has a valid subscription we recommend server-side validation. + +If you do not want to user server-side validation, it can be done by parsing receipt in the App. GdxPay has not +implemented that. Pull requests are welcome :). + +It is still possible to find out if user has a valid subscription. + +iOS keeps expired Transactions from subscriptions in it's SkPaymentTransaction queues (as apposed to Google Play, which +does not return them in the list of purchases). + +All Transactions, including historical transactions, are passed through to the `PurchaseObserver`. + +For example, if a user has a subscription with monthly period going on for 6 months and restores purchases, +6 Transactions will be passed too in PurchaseObserver#handleRestore(). + +Filter out expired purchases manually. Some pointers: + +* Start time of Transaction: `Transaction#getPurchaseTime()` +* Reference to Product Information: `Transaction#getInformation()` +* Reference to Subscription period: `Information#getSubscriptionPeriod()` + +Putting that together, you can calculate Transaction purchaseEndTime. + +If you have Billing Grace Period enabled in App Store Connect, you should add those days to the purchaseEndTime. + +This logic is covered by `Transaction#calculateSubscriptionEndDate(int billingGracePeriodInDays)` + +If there are zero transactions with your calculated purchaseEndTime available, the user has cancelled his subscription +and should resubscribe. + +Limitations of this method: + +* payment cancellations cannot be detected +* free trial periods cannot be detected; if someone decides to start a free trial of 3 days of a one-year subscription, + the user will get the full year for free if he cancels after the first day. + \ No newline at end of file diff --git a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/IosVersion.java b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/IosVersion.java index 00bf5ab9..7a1398b5 100644 --- a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/IosVersion.java +++ b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/IosVersion.java @@ -5,7 +5,7 @@ enum IosVersion { ; - static boolean isIos_7_0_orAbove() { + static boolean is_7_0_orAbove() { return Foundation.getMajorSystemVersion() >= 7; } diff --git a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java index 025ca781..93d21f95 100644 --- a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java +++ b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java @@ -22,11 +22,9 @@ import org.robovm.apple.foundation.*; import org.robovm.apple.storekit.*; +import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; /** The purchase manager implementation for Apple's iOS IAP system (RoboVM). * @@ -36,9 +34,9 @@ * */ public class PurchaseManageriOSApple implements PurchaseManager { private static final String TAG = "GdxPay/AppleIOS"; - private static final boolean LOGDEBUG = true; - private static final int LOGTYPELOG = 0; - private static final int LOGTYPEERROR = 1; + static final boolean LOGDEBUG = true; + static final int LOGTYPELOG = 0; + static final int LOGTYPEERROR = 1; private static NSNumberFormatter numberFormatter; @@ -185,72 +183,112 @@ private String getOriginalTxID(SKPaymentTransaction transaction) { /** Converts a purchase to our transaction object. */ @Nullable - Transaction transaction (SKPaymentTransaction t) { - SKPayment payment = t.getPayment(); + Transaction convertTransaction(SKPaymentTransaction iosTransaction) { + SKPayment payment = iosTransaction.getPayment(); String productIdentifier = payment.getProductIdentifier(); - SKProduct product = getProductByStoreIdentifier(productIdentifier); - if (product == null) { - // if we didn't request product information -OR- it's not in iTunes, it will be null - System.err.println("gdx-pay: product not registered/loaded: " + productIdentifier); - } - - // Build the transaction from the payment transaction object. - Transaction transaction = new Transaction(); Offer offerForStore = config.getOfferForStore(PurchaseManagerConfig.STORE_NAME_IOS_APPLE, productIdentifier); if (offerForStore == null) { System.err.println("Product not configured in PurchaseManagerConfig: " + productIdentifier + ", skipping transaction."); return null; } + final SKProduct product = getProductByStoreIdentifier(productIdentifier); - transaction.setIdentifier(offerForStore.getIdentifier()); + return new TransactionConverterWorker(iosTransaction, offerForStore, product).transaction; + } - transaction.setStoreName(PurchaseManagerConfig.STORE_NAME_IOS_APPLE); - transaction.setOrderId(getOriginalTxID(t)); + /** + * Converts {@link SKPaymentTransaction} to gdx {@link Transaction} using the worker pattern. + */ + class TransactionConverterWorker { + private final SKPaymentTransaction iosTransaction; + private final Offer offer; + private final SKProduct product; + private final Transaction transaction; + private final String productIdentifier; + private final SKPayment payment; + + public TransactionConverterWorker( + @Nonnull + SKPaymentTransaction iosTransaction, + @Nonnull + Offer offer, + @Nullable + SKProduct product) { + this.iosTransaction = iosTransaction; + this.offer = offer; + this.product = product; + this.transaction = new Transaction(); + payment = iosTransaction.getPayment(); + productIdentifier = payment.getProductIdentifier(); + + if (product == null) { + // if we didn't request product information -OR- it's not in iTunes, it will be null + System.err.println("gdx-pay: product not registered/loaded: " + productIdentifier); + } - transaction.setPurchaseTime(t.getTransactionDate().toDate()); - if (product != null) { - // if we didn't load product information, product will be 'null' (we only set if available) - transaction.setPurchaseText("Purchased: " + product.getLocalizedTitle()); - transaction.setPurchaseCost((int) Math.round(product.getPrice().doubleValue() * 100)); - transaction.setPurchaseCostCurrency(product.getPriceLocale().getCurrencyCode()); - } - else { - // product information was empty (not loaded or product didn't exist) - transaction.setPurchaseText("Purchased: " + productIdentifier); - transaction.setPurchaseCost(0); - transaction.setPurchaseCostCurrency(null); + convertCommonFields(); + convertPurchaseTextAndCost(); + convertReversalInformation(); + convertTransactionData(); + convertTransactionDataSignature(); } - transaction.setReversalTime(null); // no refunds for iOS! - transaction.setReversalText(null); + private void convertCommonFields() { + transaction.setIdentifier(offer.getIdentifier()); + transaction.setStoreName(PurchaseManagerConfig.STORE_NAME_IOS_APPLE); + transaction.setOrderId(getOriginalTxID(iosTransaction)); + transaction.setPurchaseTime(iosTransaction.getTransactionDate().toDate()); + transaction.setInformation(getInformation(offer.getIdentifierForStore(PurchaseManagerConfig.STORE_NAME_IOS_APPLE))); + } - if (payment.getRequestData() != null) { - final String transactionData; - if (IosVersion.isIos_7_0_orAbove()) { - transactionData = payment.getRequestData().toBase64EncodedString(NSDataBase64EncodingOptions.None); - } else { - transactionData = Base64.encode(payment.getRequestData().getBytes()); + private void convertTransactionDataSignature() { + // NOTE: although deprecated as of iOS 7, "transactionReceipt" is still available as of iOS 9 & hopefully long there after :) + String transactionDataSignature; + try { + NSData transactionReceipt = iosTransaction.getTransactionReceipt(); + transactionDataSignature = transactionReceipt.toBase64EncodedString(NSDataBase64EncodingOptions.None); + } catch (Throwable e) { + log(LOGTYPELOG, "SKPaymentTransaction.transactionReceipt appears broken (was deprecated starting iOS 7.0).", e); + transactionDataSignature = null; } - transaction.setTransactionData(transactionData); + transaction.setTransactionDataSignature(transactionDataSignature); } - else { - transaction.setTransactionData(null); + + private void convertTransactionData() { + if (payment.getRequestData() != null) { + final String transactionData; + if (IosVersion.is_7_0_orAbove()) { + transactionData = payment.getRequestData().toBase64EncodedString(NSDataBase64EncodingOptions.None); + } else { + transactionData = Base64.encode(payment.getRequestData().getBytes()); + } + transaction.setTransactionData(transactionData); + } + else { + transaction.setTransactionData(null); + } } - // NOTE: although deprecated as of iOS 7, "transactionReceipt" is still available as of iOS 9 & hopefully long there after :) - String transactionDataSignature; - try { - NSData transactionReceipt = t.getTransactionReceipt(); - transactionDataSignature = transactionReceipt.toBase64EncodedString(NSDataBase64EncodingOptions.None); - } catch (Throwable e) { - log(LOGTYPELOG, "SKPaymentTransaction.transactionReceipt appears broken (was deprecated starting iOS 7.0).", e); - transactionDataSignature = null; + private void convertReversalInformation() { + transaction.setReversalTime(null); // no refunds for iOS! + transaction.setReversalText(null); } - transaction.setTransactionDataSignature(transactionDataSignature); - // return the transaction - return transaction; + private void convertPurchaseTextAndCost() { + if (product != null) { + // if we didn't load product information, product will be 'null' (we only set if available) + transaction.setPurchaseText("Purchased: " + product.getLocalizedTitle()); + transaction.setPurchaseCost((int) Math.round(product.getPrice().doubleValue() * 100)); + transaction.setPurchaseCostCurrency(product.getPriceLocale().getCurrencyCode()); + } + else { + // product information was empty (not loaded or product didn't exist) + transaction.setPurchaseText("Purchased: " + productIdentifier); + transaction.setPurchaseCost(0); + transaction.setPurchaseCostCurrency(null); + } + } } private class AppleProductsDelegatePurchase extends SKProductsRequestDelegateAdapter { @@ -365,7 +403,7 @@ public void updatedTransactions (SKPaymentQueue queue, NSArray appleToGdxUnitMap = new HashMap(); + private static final Map appleToGdxUnitMap = new HashMap(); static { - appleToGdxUnitMap.put(SKProductPeriodUnit.Day, FreeTrialPeriod.PeriodUnit.DAY); - appleToGdxUnitMap.put(SKProductPeriodUnit.Week, FreeTrialPeriod.PeriodUnit.WEEK); - appleToGdxUnitMap.put(SKProductPeriodUnit.Month, FreeTrialPeriod.PeriodUnit.MONTH); - appleToGdxUnitMap.put(SKProductPeriodUnit.Year, FreeTrialPeriod.PeriodUnit.YEAR); + appleToGdxUnitMap.put(SKProductPeriodUnit.Day, SubscriptionPeriod.PeriodUnit.DAY); + appleToGdxUnitMap.put(SKProductPeriodUnit.Week, SubscriptionPeriod.PeriodUnit.WEEK); + appleToGdxUnitMap.put(SKProductPeriodUnit.Month, SubscriptionPeriod.PeriodUnit.MONTH); + appleToGdxUnitMap.put(SKProductPeriodUnit.Year, SubscriptionPeriod.PeriodUnit.YEAR); } - public static FreeTrialPeriod.PeriodUnit convertToPeriodUnit(SKProductPeriodUnit unit) { + public static SubscriptionPeriod.PeriodUnit convertToPeriodUnit(SKProductPeriodUnit unit) { return appleToGdxUnitMap.get(unit); } diff --git a/gdx-pay/src/main/java/com/badlogic/gdx/pay/Information.java b/gdx-pay/src/main/java/com/badlogic/gdx/pay/Information.java index b8e1b612..12c1d989 100644 --- a/gdx-pay/src/main/java/com/badlogic/gdx/pay/Information.java +++ b/gdx-pay/src/main/java/com/badlogic/gdx/pay/Information.java @@ -19,24 +19,28 @@ public final class Information { private final String localName; private final String localDescription; private final String localPricing; + private final SubscriptionPeriod subscriptionPeriod; /** * @deprecated Not all currencies use cents. Currencies with no or more than 2 fractional * digits exist. Use {@link #priceAsDouble} instead. */ @Deprecated - private Integer priceInCents; - private Double priceAsDouble; + private final Integer priceInCents; + private final Double priceAsDouble; - private String priceCurrencyCode; + private final String priceCurrencyCode; @Nullable - private FreeTrialPeriod freeTrialPeriod; + private final SubscriptionPeriod freeTrialPeriod; public Information(String localName, String localDescription, String localPricing) { - this.localName = localName; - this.localDescription = localDescription; - this.localPricing = localPricing; + this( + new Information.Builder() + .localName(localName) + .localDescription(localDescription) + .localPricing(localPricing) + ); } private Information(Builder builder) { @@ -47,6 +51,7 @@ private Information(Builder builder) { priceAsDouble = builder.priceAsDouble; priceCurrencyCode = builder.priceCurrencyCode; freeTrialPeriod = builder.freeTrialPeriod; + subscriptionPeriod = builder.subscriptionPeriod; } public static Builder newBuilder() { @@ -71,7 +76,7 @@ public Integer getPriceInCents() { * @return null if there is no free trial or the implementation does not support free trials. */ @Nullable - public FreeTrialPeriod getFreeTrialPeriod() { + public SubscriptionPeriod getFreeTrialPeriod() { return freeTrialPeriod; } @@ -99,7 +104,6 @@ public Double getPriceAsDouble() { /** * Price currency code. *

Caution:Note that not all PurchaseManagers set this field!

- * @return */ public String getPriceCurrencyCode() { return priceCurrencyCode; @@ -126,6 +130,11 @@ public String getLocalPricing() { return localPricing; } + @Nullable + public SubscriptionPeriod getSubscriptionPeriod() { + return subscriptionPeriod; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -162,6 +171,8 @@ public static final class Builder { private String localName; private String localDescription; private String localPricing; + private SubscriptionPeriod subscriptionPeriod; + /** * @deprecated Not all currencies use cents. Currencies with no or more than 2 fractional * digits exist. Use {@link #priceAsDouble} instead. @@ -170,7 +181,7 @@ public static final class Builder { private Integer priceInCents; private Double priceAsDouble; private String priceCurrencyCode; - private FreeTrialPeriod freeTrialPeriod; + private SubscriptionPeriod freeTrialPeriod; private Builder() { } @@ -185,7 +196,12 @@ public Builder localDescription(String val) { return this; } - public Builder freeTrialPeriod(FreeTrialPeriod val) { + public Builder subscriptionPeriod(SubscriptionPeriod val) { + subscriptionPeriod = val; + return this; + } + + public Builder freeTrialPeriod(SubscriptionPeriod val) { freeTrialPeriod = val; return this; } diff --git a/gdx-pay/src/main/java/com/badlogic/gdx/pay/Offer.java b/gdx-pay/src/main/java/com/badlogic/gdx/pay/Offer.java index 1d3f4ea7..0b185ac2 100644 --- a/gdx-pay/src/main/java/com/badlogic/gdx/pay/Offer.java +++ b/gdx-pay/src/main/java/com/badlogic/gdx/pay/Offer.java @@ -83,4 +83,8 @@ public String toString() { ", identifierForStores=" + identifierForStores + '}'; } + + public boolean isSubscription() { + return this.type == OfferType.SUBSCRIPTION; + } } diff --git a/gdx-pay/src/main/java/com/badlogic/gdx/pay/FreeTrialPeriod.java b/gdx-pay/src/main/java/com/badlogic/gdx/pay/SubscriptionPeriod.java similarity index 90% rename from gdx-pay/src/main/java/com/badlogic/gdx/pay/FreeTrialPeriod.java rename to gdx-pay/src/main/java/com/badlogic/gdx/pay/SubscriptionPeriod.java index 31793ba8..141bf4ea 100644 --- a/gdx-pay/src/main/java/com/badlogic/gdx/pay/FreeTrialPeriod.java +++ b/gdx-pay/src/main/java/com/badlogic/gdx/pay/SubscriptionPeriod.java @@ -7,7 +7,7 @@ * *

Subscriptions in App Store and Google Play can have a free trial period before starting the billing for the subscription.

*/ -public final class FreeTrialPeriod { +public final class SubscriptionPeriod { private final int numberOfUnits; @@ -36,7 +36,7 @@ public static PeriodUnit parse(char character) { } } - public FreeTrialPeriod(int numberOfUnits, PeriodUnit unit) { + public SubscriptionPeriod(int numberOfUnits, PeriodUnit unit) { this.numberOfUnits = numberOfUnits; this.unit = unit; } @@ -55,7 +55,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - FreeTrialPeriod that = (FreeTrialPeriod) o; + SubscriptionPeriod that = (SubscriptionPeriod) o; if (numberOfUnits != that.numberOfUnits) return false; return unit == that.unit; diff --git a/gdx-pay/src/main/java/com/badlogic/gdx/pay/Transaction.java b/gdx-pay/src/main/java/com/badlogic/gdx/pay/Transaction.java index 58cdada2..2bf01242 100644 --- a/gdx-pay/src/main/java/com/badlogic/gdx/pay/Transaction.java +++ b/gdx-pay/src/main/java/com/badlogic/gdx/pay/Transaction.java @@ -16,6 +16,9 @@ package com.badlogic.gdx.pay; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Calendar; import java.util.Date; /** An transaction for an item purchased via libGDX In-App payment system (IAP). @@ -57,6 +60,8 @@ public final class Transaction { /** A signature for the purchase data string for validation of the data (or null for unknown). */ private String transactionDataSignature; + private Information information = Information.UNAVAILABLE; + /** The item identifier/SKU that matches our item id in the IAP service. */ public String getIdentifier () { return identifier; @@ -103,7 +108,25 @@ public String getUserId () { public void setUserId (String userId) { this.userId = userId; } - + + /** + * + * @return the associated instance, or {@link Information#UNAVAILABLE} if it is not available or not set by + * the purchase manager. + */ + @Nonnull + public Information getInformation() { + return information; + } + + public void setInformation(Information information) { + if (information == null) { + this.information = Information.UNAVAILABLE; + } else { + this.information = information; + } + } + /** Returns true if the order is considered valid, i.e. in purchased state (non-refunded/cancelled). */ public boolean isPurchased () { return reversalTime == null; @@ -204,4 +227,45 @@ public String toString() { ", transactionDataSignature='" + transactionDataSignature + '\'' + '}'; } + + /** + * Calculate the subscription end date, based on the purchaseStartTime. + * + *

Not thoroughly tested!

+ * + * @return the end date, null if not a subscription or when fetching {@link Information}has failed. + */ + @Nullable + public Date calculateSubscriptionEndDate(int billingGracePeriodInDays) { + if (purchaseTime == null || information.getSubscriptionPeriod() == null) { + return null; + } + + final SubscriptionPeriod subscriptionPeriod = information.getSubscriptionPeriod(); + + final Calendar instance = Calendar.getInstance(); + instance.setTime(purchaseTime); + + final int amount = subscriptionPeriod.getNumberOfUnits(); + switch(subscriptionPeriod.getUnit()) { + case DAY: + instance.add(Calendar.DAY_OF_YEAR, amount); + break; + case WEEK: + instance.add(Calendar.WEEK_OF_YEAR, amount); + break; + case MONTH: + instance.add(Calendar.MONTH, amount); + break; + case YEAR: + instance.add(Calendar.YEAR, amount); + default: + System.err.println("Unsupported enum constant: " + subscriptionPeriod.getUnit()); + return null; + } + + instance.add(Calendar.DAY_OF_YEAR, billingGracePeriodInDays); + + return instance.getTime(); + } }