Skip to content
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

Upgrade/Sidegrade/Downgrade subscriptions #192

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions docs/PurchaseSubscription.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ Task<InAppBillingPurchase> PurchaseAsync(string productId, ItemType itemType, st

The `payload` attribute is a special payload that is sent and then returned from the server for additional validation. It can be whatever you want it to be, but should be a constant that is used anywhere the `payload` is used.

A subscription can also be upgraded/downgraded/sidegraded to another subscription. This implementation is Android specific because iOS handles this automatically when purchasing subscriptions from the same subscriptions group.

```csharp
/// <summary>
/// (Android specific) Upgrade/Downagrade a previously purchased subscription
/// </summary>
/// <param name="oldProductId">Sku or ID of product that needs to be upgraded</param>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="payload">Developer specific payload (can not be null)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string oldProductId, string newProductId, string payload, IInAppBillingVerifyPurchase verifyPurchase = null);
```

Example:
```csharp
public async Task<bool> PurchaseItem(string productId, string payload)
Expand Down
27 changes: 19 additions & 8 deletions src/Plugin.InAppBilling.Abstractions/BaseInAppBilling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,24 @@ public async Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(ItemType
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
public abstract Task<InAppBillingPurchase> PurchaseAsync(string productId, ItemType itemType, string payload, IInAppBillingVerifyPurchase verifyPurchase = null);

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
public abstract Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string purchaseToken);
/// <summary>
/// (Android specific) Upgrade/Downagrade a previously purchased subscription
/// </summary>
/// <param name="oldProductId">Sku or ID of product that needs to be upgraded</param>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="payload">Developer specific payload (can not be null)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
public abstract Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string oldProductId, string newProductId, string payload, IInAppBillingVerifyPurchase verifyPurchase = null);

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
public abstract Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string purchaseToken);

/// <summary>
/// Consume a purchase
Expand Down Expand Up @@ -137,5 +147,6 @@ public virtual void Dispose(bool disposing)
public virtual Task<bool> FinishTransaction(InAppBillingPurchase purchase) => Task.FromResult(true);

public virtual Task<bool> FinishTransaction(string purchaseId) => Task.FromResult(true);

}
}
27 changes: 19 additions & 8 deletions src/Plugin.InAppBilling.Abstractions/IInAppBilling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,25 @@ public interface IInAppBilling : IDisposable
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
Task<InAppBillingPurchase> PurchaseAsync(string productId, ItemType itemType, string payload, IInAppBillingVerifyPurchase verifyPurchase = null);

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string purchaseToken);
/// <summary>
/// (Android specific) Upgrade/Downagrade a previously purchased subscription
/// </summary>
/// <param name="oldProductId">Sku or ID of product that needs to be upgraded</param>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="payload">Developer specific payload (can not be null)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string oldProductId, string newProductId, string payload, IInAppBillingVerifyPurchase verifyPurchase = null);

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string purchaseToken);

/// <summary>
/// Consume a purchase
Expand Down
127 changes: 125 additions & 2 deletions src/Plugin.InAppBilling.Android/InAppBillingImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public class InAppBillingImplementation : BaseInAppBilling
const string ITEM_TYPE_INAPP = "inapp";
const string ITEM_TYPE_SUBSCRIPTION = "subs";

const string RESPONSE_CODE = "RESPONSE_CODE";
const string EXTRA_SUB_PARAM_REPLACED_SKUS = "skusToReplace";


const string RESPONSE_CODE = "RESPONSE_CODE";
const string RESPONSE_BUY_INTENT = "BUY_INTENT";
const string RESPONSE_IAP_DATA = "INAPP_PURCHASE_DATA";
const string RESPONSE_IAP_DATA_SIGNATURE = "INAPP_DATA_SIGNATURE";
Expand Down Expand Up @@ -281,7 +284,127 @@ public async override Task<InAppBillingPurchase> PurchaseAsync(string productId,
};
}

async Task<Purchase> PurchaseAsync(string productSku, string itemType, string payload, IInAppBillingVerifyPurchase verifyPurchase)
/// <summary>
/// (Android specific) Upgrade/Downagrade a previously purchased subscription
/// </summary>
/// <param name="oldProductId">Sku or ID of product that needs to be upgraded</param>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="payload">Developer specific payload (can not be null)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
public async override Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string oldProductId, string newProductId, string payload, IInAppBillingVerifyPurchase verifyPurchase = null)
{
if (payload == null)
throw new ArgumentNullException(nameof(payload), "Payload can not be null");


if (serviceConnection?.Service == null)
{
throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable, "You are not connected to the Google Play App store.");
}

Purchase purchase = await UpgradePurchasedSubscriptionInternalAsync(oldProductId, newProductId, payload, verifyPurchase);

if (purchase == null)
return null;

var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

return new InAppBillingPurchase
{
TransactionDateUtc = epoch + TimeSpan.FromMilliseconds(purchase.PurchaseTime),
Id = purchase.OrderId,
AutoRenewing = purchase.AutoRenewing,
PurchaseToken = purchase.PurchaseToken,
State = purchase.SubscriptionState,
ConsumptionState = purchase.ConsumedState,
ProductId = purchase.ProductId,
Payload = purchase.DeveloperPayload ?? string.Empty
};
}

async Task<Purchase> UpgradePurchasedSubscriptionInternalAsync(string oldProductId, string newProductId, string payload, IInAppBillingVerifyPurchase verifyPurchase = null)
{
string itemType = ITEM_TYPE_SUBSCRIPTION;

lock (purchaseLocker)
{
if (tcsPurchase != null && !tcsPurchase.Task.IsCompleted)
return null;

Bundle extraParams = new Bundle();
extraParams.PutStringArrayList(EXTRA_SUB_PARAM_REPLACED_SKUS, new List<string> { oldProductId });
Bundle buyIntentBundle = serviceConnection.Service.GetBuyIntentExtraParams(6, Context.PackageName, newProductId, itemType, payload, extraParams);
var response = GetResponseCodeFromBundle(buyIntentBundle);

switch (response)
{
case 0:
//OK to purchase
break;
case 1:
//User Cancelled, should try again
throw new InAppBillingPurchaseException(PurchaseError.UserCancelled);
case 2:
//Network connection is down
throw new InAppBillingPurchaseException(PurchaseError.ServiceUnavailable);
case 3:
//Billing Unavailable
throw new InAppBillingPurchaseException(PurchaseError.BillingUnavailable);
case 4:
//Item Unavailable
throw new InAppBillingPurchaseException(PurchaseError.ItemUnavailable);
case 5:
//Developer Error
throw new InAppBillingPurchaseException(PurchaseError.DeveloperError);
case 6:
//Generic Error
throw new InAppBillingPurchaseException(PurchaseError.GeneralError);
case 7:
//already purchased
throw new InAppBillingPurchaseException(PurchaseError.AlreadyOwned);
}


var pendingIntent = buyIntentBundle.GetParcelable(RESPONSE_BUY_INTENT) as PendingIntent;
if (pendingIntent == null)
throw new InAppBillingPurchaseException(PurchaseError.GeneralError);

tcsPurchase = new TaskCompletionSource<PurchaseResponse>();

Context.StartIntentSenderForResult(pendingIntent.IntentSender, PURCHASE_REQUEST_CODE, new Intent(), 0, 0, 0);
}

var result = await tcsPurchase.Task;

if (result == null)
return null;

var data = result.PurchaseData;
var sign = result.DataSignature;

//for some reason the data didn't come back
if (string.IsNullOrWhiteSpace(data))
{
var purchases = await GetPurchasesAsync(itemType, verifyPurchase);

var purchase = purchases.FirstOrDefault(p => p.ProductId == newProductId && payload.Equals(p.DeveloperPayload ?? string.Empty));

return purchase;
}

if (verifyPurchase == null || await verifyPurchase.VerifyPurchase(data, sign))
{
var purchase = JsonConvert.DeserializeObject<Purchase>(data);
if (purchase.ProductId == newProductId && payload.Equals(purchase.DeveloperPayload ?? string.Empty))
return purchase;
}

return null;

}

async Task<Purchase> PurchaseAsync(string productSku, string itemType, string payload, IInAppBillingVerifyPurchase verifyPurchase)
{
lock (purchaseLocker)
{
Expand Down
29 changes: 21 additions & 8 deletions src/Plugin.InAppBilling.UWP/InAppBillingImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,27 @@ public async override Task<InAppBillingPurchase> PurchaseAsync(string productId,

}

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
public async override Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string purchaseToken)
/// <summary>
/// (Android specific) Upgrade/Downagrade a previously purchased subscription
/// </summary>
/// <param name="oldProductId">Sku or ID of product that needs to be upgraded</param>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="payload">Developer specific payload (can not be null)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
public async override Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string oldProductId, string newProductId, string payload, IInAppBillingVerifyPurchase verifyPurchase = null)
{
throw new NotImplementedException("UWP not supported. Windows store can't manage subscriptions upgrades.");
}

/// <summary>
/// Consume a purchase with a purchase token.
/// </summary>
/// <param name="productId">Id or Sku of product</param>
/// <param name="purchaseToken">Original Purchase Token</param>
/// <returns>If consumed successful</returns>
/// <exception cref="InAppBillingPurchaseException">If an error occures during processing</exception>
public async override Task<InAppBillingPurchase> ConsumePurchaseAsync(string productId, string purchaseToken)
{
var result = await CurrentAppMock.ReportConsumableFulfillmentAsync(InTestingMode, productId, new Guid(purchaseToken));
switch(result)
Expand Down
15 changes: 14 additions & 1 deletion src/Plugin.InAppBilling.iOS/InAppBillingImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,20 @@ public async override Task<InAppBillingPurchase> PurchaseAsync(string productId,
return validated ? purchase : null;
}

Task<bool> ValidateReceipt(IInAppBillingVerifyPurchase verifyPurchase, string productId, string transactionId)
/// <summary>
/// (Android specific) Upgrade/Downagrade a previously purchased subscription
/// </summary>
/// <param name="oldProductId">Sku or ID of product that needs to be upgraded</param>
/// <param name="newProductId">Sku or ID of product that will replace the old one</param>
/// <param name="payload">Developer specific payload (can not be null)</param>
/// <param name="verifyPurchase">Verify Purchase implementation</param>
/// <returns>Purchase details</returns>
public async override Task<InAppBillingPurchase> UpgradePurchasedSubscriptionAsync(string oldProductId, string newProductId, string payload, IInAppBillingVerifyPurchase verifyPurchase = null)
{
throw new NotImplementedException("iOS not supported. Apple store manages upgrades natively when subscriptions of the same group are purchased.");
}

Task<bool> ValidateReceipt(IInAppBillingVerifyPurchase verifyPurchase, string productId, string transactionId)
{
if (verifyPurchase == null)
return Task.FromResult(true);
Expand Down
Loading