This is a one-stop and end-to-end solution for in-app purchases and subscriptions for solutions consisting of Zebble mobile apps and Olive server apps. To understand how it all works in different platforms, read this introduction.
In the Zebble mobile app, install the appropriate nuget packages on all your supported platforms:
- Zebble.Billing add to all projects
- Zebble.Billing.CafeBazaar add to Android if you need to support CafeBazaar
Then you should initialize the plugin in your app's StartUp class like below:
public partial class StartUp : Zebble.StartUp
{
public override async Task Run()
{
// ...
BillingContext.Initialize();
// Try to fetch products' prices from the store.
Thread.Pool.RunOnNewThread(BillingContext.Current.UpdateProductPrices);
// Ask the server for the latest subscription status.
Thread.Pool.RunOnNewThread(BillingContext.Current.BackgroundRefresh);
// ...
}
}
The BillingContext.Initialize
call (without providing any argument), leads to the default options and you need to add on of the following lines to your Config.xml
file.
<!-- This line has higher precedence -->
<Billing.Base.Url value="http://<YOUR_SERVER_URL>" />
<!-- This will be used as the fallback url -->
<Api.Base.Url value="http://<YOUR_SERVER_URL>" />
Then create a JSON file named Catalog.json
in the Resources directory and set the following properties:
Build Action
: Embedded resource
Copy to Output Directory
: Copy if newer
Thereupon, add all your products into it. Products are either one-off or they are recurring payments. Recurring products are called Subscriptions, and one-off products are called InApp-Products. Regardless, each product should be defined in the appropriate app store first (Google Play, Apple iTunes Connect, etc) with the appropriate settings.
{
"Products": [
{
"Id": "my.app.subscription.yearly",
"Platform": "",
"Type": "Subscription"
}
]
}
The products need to be filled according to the products you've defined in your supporting stores, especially Id
, and Type
.
If any of your products are platform-specific, you need to fill Platform
with one of the following values: [AppStore, GooglePlay, CafeBazaar, WindowsStore], otherwise leave it empty.
For InAppPurchase products, fill Type
with InAppPurchase
, otherwise use Subscription
.
To override defaults, you can create an instance of BillingContextOptions
and pass it to the BillingContext.Initialize
. Here are the list of the properties you can provide:
BaseUri
: Assigning a Uri to this prop will specify the base URL for any server invocations. Bear in mind, when you assign a value for this prop, you don't need to add Billing.Base.Url
to your Config.xml
file.
PurchaseAttemptPath
: The relative path to purchase attempt endpoint. The default value is "app/purchase-attempt".
VoucherApplyPath
: The relative path to voucher apply endpoint. The default value is "voucher/apply".
SubscriptionStatusPath
: The relative path to subscription status endpoint. The default value is "app/subscription-status".
CatalogPath
: The path of the catalog file in the client app. The default value is "Catalog.json".
In the server-side ASP.NET Core app, install the following packages:
- Zebble.Billing.Server the base package used by all the following providers, so you don't need a direct reference to this package.
- Zebble.Billing.Server.GooglePlay to support Google Play store.
- Zebble.Billing.Server.AppStore to support Apple (iOS) via iTunes.
- Zebble.Billing.Server.CafeBazaar to support Cafe Bazaar store.
- Zebble.Billing.Server.Voucher to support direct sales (outside of the app stores).
All the above providers need to collaborate with a data persistence implementation. At the moment, we're supporting EntityFramework (Sql Server) and Amazon's DynamoDb, and we'll try to add built-in support for other options soon. Also any contribution to add other persisting options are welcome.
- Zebble.Billing.Server.EntityFramework to use RDBMS for subscription management
- Zebble.Billing.Server.Voucher.EntityFramework to use RDBMS for voucher management
- Zebble.Billing.Server.DynamoDb to use Amazon's DynamoDb for subscription management
- Zebble.Billing.Server.Voucher.DynamoDb to use Amazon's DynamoDb for voucher management
Then add the required configuration and files from this sample app.
This is the sample settings file we included in the project to clearly show you what you need to set your web app up and running.
{
"ZebbleBilling": {
"Catalog": {
"Products": [
{
"Id": "my.app.subscription.yearly",
"Platform": ""
}
]
},
"EntityFramework": {
"ConnectionString": "Database=Billing.Sample; Server=.; Integrated Security=SSPI; MultipleActiveResultSets=True;"
},
"DynamoDb": {
"Profile": "<AWS_PROFILE_NAME>",
"Region": "<AWS_REGION>"
},
"AppStore": {
"PackageName": "<ios.package.name>",
"SharedSecret": "<APP_STORE_SHARED_SECRET>",
"Environment": "<Sandbox | Production>",
"HookInterceptorUri": "app-store/intercept-hook"
},
"GooglePlay": {
"PackageName": "<play.package.name>",
"QueueProcessorUri": "google-play/process-queue",
"Store": {
"ProjectId": "<PROJECT_ID>",
"PrivateKeyId": "<PRIVATE_KEY_ID>",
"PrivateKey": "<PRIVATE_KEY>",
"ClientEmail": "<CLIENT_EMAIL>",
"ClientId": "<CLIENT_ID>"
},
"PubSub": {
"ProjectId": "<PROJECT_ID>",
"PrivateKeyId": "<PRIVATE_KEY_ID>",
"PrivateKey": "<PRIVATE_KEY>",
"ClientEmail": "<CLIENT_EMAIL>",
"ClientId": "<CLIENT_ID>",
"SubscriptionId": "<SUBSCRIPTION_ID>"
}
},
"CafeBazaar": {
"PackageName": "<bazaar.package.name>",
"DeveloperApi": {
"RedirectUri": "cafe-bazaar/auth-callback",
"ClientId": "<CLIENT_ID>",
"ClientSecret": "<CLIENT_SECRET>"
}
}
}
}
Catalog
: This is the sample as the Catalog.json
file we've talked about it earlier.
EntityFramework
: The connection string used in both Zebble.Billing.Server.EntityFramework
and Zebble.Billing.Server.Voucher.EntityFramework
packages.
DynamoDb
: Possible AWS options used in both Zebble.Billing.Server.DynamoDb
and Zebble.Billing.Server.Voucher.DynamoDb
packages.
AppStore:PackageName
: Your iOS app package name.
AppStore:SharedSecret
: Your App Store connect shared secret. Follow this article to learn how you can create a shared secret.
AppStore:Environment
: Use Sandbox
when you're test-flighting your app, otherwise use Production
. We do not allow mixed receipt validation. So if you configure it for the production environment, and attempt to validate a sandbox-based receipt, the whole process will be rejected.
AppStore:HookInterceptorUri
: The relative path for hook interceptor middleware. Whatever path you specified for this has to be used in your App Store settings. Follow this article to learn how you can set a URL for App Store Server Notifications.
GooglePlay:PackageName
: Your Android app package name.
GooglePlay:QueueProcessorUri
: The relative path for queue processor middleware. You need to call this endpoint periodically, to keep your Google Play purchases in-sync with your database. Read this article to learn how Google notifications should be configured. Also, this article will help you configure billing's real-time developer notifications (RTDN).
GooglePlay:ProjectId
, GooglePlay:PrivateKeyId
, GooglePlay:PrivateKey
, GooglePlay:ClientEmail
, GooglePlay:ClientId
: First of all, follow this article to configure a service account with appropriate permissions. After you've created your service account, you need to add a new JSON key by following this article. Finally, open the provided JSON file with your preferred text-editor of choice, find and copy-paste all required values into their corresponding placeholders.
GooglePlay:SubscriptionId
: Provide the name of the Pub/Sub subscription you've created earlier.
The DynamoDb providers are utilizing AWSSDK.Extensions.NETCore.Setup
package to set the required AWS DynamoDb stuff up and configured. This means you have to follow Amazon's best practices to configure credentials to be able to use the live version of the DynamoDb. But there is an easy-to-setup solution for local testing. Please follow this blog post to find how you can bootstrap a local instance of DynamoDb on your development machine.
To trigger the purchase of a product, you need to call BillingContext.Current.PurchaseSubscription
and provide the Id
of a product. It's obvious that the product must be predefined.
async Task OnPurchaseTap(string id)
{
var result = await BillingContext.Current.PurchaseSubscription(id);
if (result == PurchaseResult.Succeeded) { /* Purchase was succeeded */ }
else if (result == PurchaseResult.WillBeActivated) { /* Purchase will be activated soon */ }
else if (result == PurchaseResult.Cancelled) { /* The purchase was cancelled */ }
else await Zebble.Alert.Show("Error", result.ToString());
}
GetProducts
: Gets the list of the predefined products.
GetProduct
: Gets a product by its Id
.
GetPrice
: Gets a product's price by its Id
.
GetLocalPrice
: Gets a product's local price by its Id
.
UpdateProductPrices
: Fetches and stores the latest prices from the store. An active internet connection is required.
RestoreSubscription
: Restores all already purchased subscriptions. If you pass the true for userRequest
, and no active subscription is found, it will throw an exception.
Refresh
: Queries the latest subscription status from the server.
BackgroundRefresh
: Queries the latest subscription status from the server in background. An active internet connection is required.
IsStarted
: True if found any subscription and it's started, otherwise False.
IsExpired
: True if found any subscription and it's expired, otherwise False.
IsCanceled
: True if found any subscription and it's cancelled, otherwise False.
IsSubscribed
: True if found any subscription and it's started but not expired and not canceled, otherwise False.
CurrentProduct
: The product instance if found any subscription with an attached product, otherwise null.
CurrentProductId
: The product's id if found any subscription with an attached product, otherwise null.
We will update this list with common purchase-related issues you might face when testing your app.
Please ensure you've unchecked "Interrupt Purchases for This Tester" for your tester account. To verify it, go to the Testers and edit your tester account.
Try to disable your Play Store app. Disabling it will remove its associated data. Then reopen it and it'll automatically updates itself. Then try to test again.