Skip to content

Product stores

After each purchase, the app receives a token which is sent to backend. It then talks to the respective store and verifies this token. If valid, it then assigns a subscription or one-time product to the given user. In Group Premium, it also makes that group Premium.

Subscriptions are slightly more complicated because we need to track whether they are still active to update our UI (and allow reactivating). App Store & Play Store both have a defined callback URL which is triggered when the status of a subscription changes. Stripe doesn't have this callback now because there is no UI outside of Settle Up where users can cancel their subscription.

Backend also iterates over all subscriptions about to expire / recently expired and tries to verify them. After a configured time period it gives up and stops trying to verify.

Naming convention for new products

Subscriptions suffer from legacy approaches. Currently, there is premium_yearly in Google Play Store, YearlyIndividualRenewable in App Store Connect, and yearly in Stripe. One-time products are newer and therefore more similar across mobile platforms (group_premium_31), and group_31 in Stripe.

These discrepancies do not pose great nuisance since their names are still understandable, however, should any new products or tiers be added, their naming should follow a unified convention. Say, we want to add a weekly Premium subscription. Its ID should then be similar across all platforms, for example, premium_weekly. If we decide to create a whole new tier called Platinum, its ID might be platinum_yearly. One should not include extra words such as "Renewable" (because the product is listed under Subscriptions, so it's inherently renewable), or lose the product type ("weekly" is simply not enough).

Stripe offers

To configure a Stripe offer (which is already seeded in our database) read more in Campaigns.

App Store offers

Promotional offers require a signature certifying the user's eligibility. iOS app needs to call the generateOfferSignature callable function with payload

{
  productId: string;
  offerId: string;
  // set to true for StoreKit 2 clients, which don't pass an appAccountToken
  // (Firebase uid is not a UUID) — the signature is then computed with an empty applicationUsername
  storeKit2?: boolean;
}

The function returns the following object:

export interface OfferSignatureResponse {
  nonce: string;
  timestamp: number;
  keyId: string;
  signature: string;
}