Options
All
  • Public
  • Public/Protected
  • All
Menu

npm (tag) GitHub Workflow Status (branch) codecov

dolla bill

Easily work with Apple in-app purchases. So you can more easily collect your "Dolla dolla bills, ya'll".

dolla bill makes it very easy to...

  • Verify receipts from StoreKit
  • Process server-to-server notifications

Note: If you have already written the code to verify receipts from Apple, you may be interested in Typescript type definitions for Apple's responses.

logo image. Sloth meme.

What is dolla bill?

When implementing Apple in-app purchase receipt validation, it takes quite a bit of work (especially auto-renewable subscriptions!). There are many steps:

  1. Implementing StoreKit in your app to allow your customers to make purchases.
  2. Implement server side validation of Receipts to validate the transactions are not fake and unlock access to paid content to your customers.
  3. If offering auto-renewable subscriptions, you need to continuously keep track of the status of your customers and give them or restrict access to paid content as time goes on. This requires you have your own database of customers on your backend server.

dolla bill takes care of step #2 above. From my experience, step #2 is the most work and biggest pain!

Dolla bill helps you to:

  • Perform the HTTP request to Apple's servers.
  • If an error happens, dolla bill processes errors and returns back a helpful error message to you, the developer, to help you fix the problem for easily.
  • Parse successful responses from Apple into a very handy API for you to use.

Example: Let's say that you need to determine if a customer is eligible to receive a discount on a new subscription.

Before dolla bill...

const transactions = parseAppleResponse(responseBody).transactions // parse Apple HTTP response to get transactions 
const subscriptions = transactions.filter(...) // get only the subscription transactions
const subscriptionGroups = ... // group all of the subscriptions into different groups 
const isSubscriptionGroupEligibleForDiscount = subscriptionGroups.transactions.forEach(transaction => transaction.in_trial_period)

Note: The code above is pseudocode. The real code is at least double the amount of work

After dolla bill...

dollaBillResponse.autoRenewableSubscriptions[0].isEligibleIntroductoryOffer

dolla bill parsed the results back from Apple and just hands to you the info that you actually care about.

That's just 1 example. You also need to write code to determine...

  • Is my customer in a grace period? If so, how long?
  • Is my customer beyond a grace period but still in billing retry?
  • Will my customer auto-renew their subscription?
  • ... and many more

Why use dolla bill?

When you are accepting in-app purchases in an iOS mobile app, you need to verify the transaction and continuously update your customer's subscription status. This verification and parsing of the responses back from Apple is boilerplate. This small module exists to help you with these tasks.

  • No dependencies. This module tries to be as small as possible.
  • Fully tested. Automated testing (with high test coverage) + QA testing gives you the confidence that the module will work well for you. codecov
  • Complete API documentation to more easily use it in your code.
  • Up-to-date If and when Apple changes the way you perform verification or Apple adds more features to verification, update this module and your projects can take advantage of the changes quickly and easily.
  • Typescript typings This project is written in Typescript for that strict typing goodness.

Note: Technically there is one dependency but it's a tiny wrapper around a built-in nodejs feature. This module still remains super small with this.

Getting started

Verify Receipt

  1. First, you need to send the receipt that StoreKit has given you to dollabill for verifying:
import {verifyReceipt, isFailure, AutoRenewableSubscription} from "dollabill-apple"
// Note: The typescript typings for the raw Apple responses are in the npm module: types-apple-iap
// Full API documentation is also available for those typings: https://github.com/levibostian/types-apple-iap/#documentation
import { AppleReceipt } from "types-apple-iap"

const receiptFromStoreKit = // base64 encoded string StoreKit has given you in your app. 

// It's optional to wrap verifyReceipt() in a try/catch. 
// If an error is thrown, more then likely it's a bug with our module, not your code. 
// All errors that *could* happen are caught for you and returned from the Promise.
const appleResponse = await verifyReceipt({
  receipt: receiptFromStoreKit,
  sharedSecret: process.env.APP_STORE_CONNECT_SHARED_SECRET // https://stackoverflow.com/a/56128978/1486374
})

if (isFailure(appleResponse)) { 
  // There was a problem. The receipt was not valid, the shared secret you passed in didn't match. The receipt is not valid. Apple's servers were down. 
  // It's recommended that you, the developer, log the error. We have tried to make the error messages developer friendly to help you fix the problem. 
  // It's *not* recommended that you return the error to your customer. You should return back your own message as the error here is not human friendly. 
} else {
  // The receipt has been verified by Apple and parsed by the module! Yay!

  // API reference for the result object: https://levibostian.github.io/dollabill-apple/api/interfaces/parsedreceipt.html

  // Time for you to update your database with the status of your customer and their subscription.
  // This is easy because dolla bill parses the response from Apple to be easily readable.
  // Check out the API documentation to learn about what `appleResponse` properties there are.

  handleSuccessfulResult(appleResponse)
}
  1. Then, you need to write all of your logic for handling the response from dollabill.
// dollabill produces the same result when verifying receipts *and* 
// parsing server-to-server notifications. This means you can re-use
// your app's logic for handling the result of each!
const handleSuccessfulResult = (result: ParsedResult): void => {
  const subscriptions: AutoRenewableSubscription[] = result.autoRenewableSubscriptions
  // You shouldn't need to, but you can access the raw response from Apple. 
  // Hopefully, dollabill has parsed enough helpful information for you that you don't need to use this. It's here just in case. 
  result.rawResponse

  console.log(`App where purchase was made: ${result.bundleId}`)

  // API for subscription: https://levibostian.github.io/dollabill-apple/api/interfaces/autorenewablesubscription.html
  subscriptions.forEach(subscription => {
    // It's up to you to write all of the code needed to update your customer's subscription statuses. At this time, Apple recommended you maintain a database stored with:
    // 1. The original transaction id for each subscription: 
    subscription.originalTransactionId
    // 2. The current end date that the subscription is valid for. You can use: 
    subscription.expireDate
    // but this does not account for grace periods or refund/cancellations. Dolla bill provides a convenient Date property instead:
    subscription.currentEndDate
    // 3. Keep track if the customer is eligible for a subscription offer or not. 
    subscription.isEligibleIntroductoryOffer
    // Eligibility for intro offers is grouped by the subscription group, not by the original transaction id. So make sure you keep track of the subscription group, too:
    subscription.subscriptionGroup
    // 4. Keep the latest receipt in case you need to call `verifyReceipt()` in the future to get a status of the customer's subscription:
    result.latestReceipt

    // Beyond these basics, there are more advanced things you can do. Some things to help reduce the number of customers leaving your subscription.
    subscription.issues.notYetAcceptingPriceIncrease // Customer has been notified about price increase but not accepting it yet. 
    subscription.issues.willVoluntaryCancel          // Customer will not automatically renew 
    subscription.issues.billingIssue                 // Customer is having billing issues with renewing. 

    subscription.status // Gives a status telling you quickly what state the subscription is in. 
  })
}

Server-to-server notification

Super easy, especially if you have already written your code for verifying receipts.

// Note: The typescript typings for the raw Apple responses are in the npm module: types-apple-iap
// Full API documentation is also available for those typings: https://github.com/levibostian/types-apple-iap/#documentation
import { AppleServerNotificationResponseBody } from "types-apple-iap"

const notification: AppleServerNotificationResponseBody = // server notification sent to you by Apple 

const parsedResult = parseServerToServerNotification({
  sharedSecret: "foo",
  responseBody: notification
})

if (isFailure(parsedResult)) {
  // There was a problem. More then likely the shared secret you provided does not match the one in the notification. This means you, the developer, made a typo or there is a malicious person trying to send you fake notifications. 

  console.error(parsedResult)
} else {
  // The notification has been parsed successfully!

  // The result type is the same as the result type of `verifyReceipt()` which means that you can 
  // re-use the same logic in your app to update the customer's subscription status! 
  handleSuccessfulResult(parsedResult)
}

Documentation

Full API documentation is hosted here. The documentation, as of now, is a little busy because it includes all code including internal code not meant for you to use.

Convenient quick links:

Tests

Debug in VSCode

The example project is setup to be debuggable.

First, run npm run build to build the module. The example requires it. Then, you can go into the debugger view in VSCode and run "Debug example" task. It will trigger breakpoints in the module code or the example code.

Automated tests

npm run test

QA testing

  • First, you need a receipt. To get that, you need a paid Apple developer membership account (because you cannot generate a Receipt without access to App Store Connect).

To create a Receipt, follow these steps:

  1. Go into App Store Connect and create an example app if you do not have one already. Create subscriptions, consumables, etc. Whatever you want. Also set a shared secret which is found in the subscriptions section of your app in App Store Connect.
  2. Download and run the example iOS app from Apple

Find this code in the app:

    func paymentQueue(_ queue: SKPaymentQueue,updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
          switch transaction.transactionState {
              case .purchased: // <-------- This line is what we are looking for
          }
        }
        //Handle transaction states here.
    }

Add this code:

case .purchased: // <----------- This case is what we are adding to.
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
    FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {

    do {
        let rawReceiptData = try Data(contentsOf: appStoreReceiptURL)
        let receipt = rawReceiptData.base64EncodedString(options: [])

        print("Receipt: \(receipt)")

        queue.finishTransaction(transaction)        
    }
    catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}  
  1. Run the example app on your computer in simulator or device as a development build. Make a purchase. You will see in the XCode console a line Receipt: XXXXXXX. Copy the very long string that is printed. That's your receipt.

  2. Run: cp example/secrets.json.example example/secrets.json and then go into example/secrets.json and edit it to your receipt and shared secret you just got.

  3. Time to run the QA test! Pretty easy.

npm run qa:setup # You only need to run this once for setup.
npm run qa       # Run the actual test 

You should expect the script to output to the console JSON of the parsed result.

Contributors

Thanks goes to these wonderful people (emoji key)


Levi Bostian

💻 📖 🚧

Index

Type aliases

AutoRenewableSubscriptionIssueString

AutoRenewableSubscriptionIssueString: "not_yet_accepting_price_increase" | "billing_issue" | "will_voluntary_cancel"

See {@link AutoRenewableSubscriptionActionableStatus}

AutoRenewableSubscriptionRefundReason

AutoRenewableSubscriptionRefundReason: "upgrade" | "app_issue" | "other"

AutoRenewableSubscriptionStatus

AutoRenewableSubscriptionStatus: "active" | "billing_retry_period" | "grace_period" | "voluntary_cancel" | "upgraded" | "involuntary_cancel" | "refunded" | "other_not_active"

Options:

  1. active - when no issue is detected, the subscription is active. There are other statuses, however, that determine if the subscription is still a valid one (such as "grace_period").
  2. billing_retry_period - When the subscription is beyond the grace period, but Apple is still trying to restore the purchase.
  3. voluntary_cancel - they cancelled the subscription. You should not give the customer any access to the paid content.
  4. grace_period - There is a billing issue and Apple is attempting to automatically renew the subscription. Grace period means the customer has not cancelled or refunded. You have not yet lost them as a customer.

Tip: You can open the link https://apps.apple.com/account/billing to send the user to their payment details page in the App Store to update their payment information.

  1. involuntary_cancel - the customer had a billing issue and their subscription is no longer active. Maybe you (1) did not enable the Grace Period feature in App Store Connect and the customer has encountered a billing issue or (2) you did enable the Grace Period feature but Apple has since given up on attempting to renew the subscription for the customer. You should no longer give the customer access to the paid content.
  2. refunded - the customer contacted Apple support and asked for a refund of their purchase and received the partial or full refund. You should not give the customer any access to the paid content. You may optionally offer another subscription plan to them to switch to a different offer that meets their needs better.
  3. other_not_active - attempt to be future-proof as much as possible. active is only if we confirm that there are not cancellations, no expiring, no billing issues, etc. But if Apple drops on us some random new feature or expiration intent that this library doesn't know how to parse yet (or this version you have installed doesn't yet) you will get in the catch-all other_not_active.
  4. upgraded - this subscription is no longer active, it has been cancelled because the customer upgraded to a new subscription group level and created a new subscription with that.

AutoRenewableSubscriptionTransaction

AutoRenewableSubscriptionTransaction: { cancelledDate?: Date; expiresDate: Date; inIntroPeriod: boolean; inTrialPeriod: boolean; isFirstSubscriptionPurchase: boolean; isRenewOrRestore: boolean; isUpgradeTransaction: boolean; offerCodeRefName?: undefined | string; originalPurchaseDate: Date; originalTransactionId: string; productId: string; promotionalOfferId?: undefined | string; purchaseDate: Date; rawTransaction: AppleLatestReceiptInfo; refundReason?: AutoRenewableSubscriptionRefundReason; subscriptionGroupId: string; transactionId: string; webOrderLineItemId?: undefined | string }

Receipt for an auto-renewable subscription.

Type declaration

  • Optional cancelledDate?: Date
  • expiresDate: Date
  • inIntroPeriod: boolean
  • inTrialPeriod: boolean
  • isFirstSubscriptionPurchase: boolean
  • isRenewOrRestore: boolean
  • isUpgradeTransaction: boolean
  • Optional offerCodeRefName?: undefined | string
  • originalPurchaseDate: Date
  • originalTransactionId: string
  • productId: string
  • Optional promotionalOfferId?: undefined | string
  • purchaseDate: Date
  • rawTransaction: AppleLatestReceiptInfo
  • Optional refundReason?: AutoRenewableSubscriptionRefundReason
  • subscriptionGroupId: string
  • transactionId: string
  • Optional webOrderLineItemId?: undefined | string

ParseResult

ParseResult: ParsedResult | Error

The response type of verifyReceipt. A failure or a success.

Make sure to use isFailure after you get this response.

Variables

Const appleProductionVerifyReceiptEndpoint

appleProductionVerifyReceiptEndpoint: "https://buy.itunes.apple.com/verifyReceipt" = "https://buy.itunes.apple.com/verifyReceipt"
internal

Const appleSandboxVerifyReceiptEndpoint

appleSandboxVerifyReceiptEndpoint: "https://sandbox.itunes.apple.com/verifyReceipt" = "https://sandbox.itunes.apple.com/verifyReceipt"
internal

Const bugReportLink

bugReportLink: "https://github.com/levibostian/dollabill-apple/issues/new?template=BUG_REPORT.md" = `https://github.com/levibostian/dollabill-apple/issues/new?template=BUG_REPORT.md`
internal

Functions

Const handleErrorResponse

  • handleErrorResponse(response: AppleVerifyReceiptResponseBodyError): Error
  • Parses the HTTP response and returns back an Error meant for a developer's purpose. These errors below are meant to be more friendly to help the developer fix the issue.

    internal

    Parameters

    • response: AppleVerifyReceiptResponseBodyError

    Returns Error

Const isAppleErrorCode

  • isAppleErrorCode(code: number): boolean

Const isAppleResponseError

  • isAppleResponseError(response: AppleVerifyReceiptResponseBody): response is AppleVerifyReceiptResponseBodyError

Const isFailure

  • Determine if a {@link VerifyReceiptResponse} was a failed response or a success.

    Parameters

    Returns response is Error

Const parsePurchases

  • parsePurchases(latestReceiptInfo?: AppleLatestReceiptInfo[], inAppTransactions?: AppleInAppPurchaseTransaction[]): ProductPurchases[]
  • Here, we are parsing the latest receipt info and in-app purchase transactions.

    This is because (1) I am still confused by Apple's documentation on what one to use, when. (2) the latest receipt info contains the latest transactions which is great, but the latest receipt info is only available when the receipt contains auto-renewable subscriptions. (3) This document: https://developer.apple.com/documentation/appstoreservernotifications/unified_receipt says that latest_receipt_info only contains the latest 100 receipt transactions. So, I also want to process the in_app transactions, too.

    internal

    Parameters

    • Optional latestReceiptInfo: AppleLatestReceiptInfo[]
    • Optional inAppTransactions: AppleInAppPurchaseTransaction[]

    Returns ProductPurchases[]

Const parseResponseBody

  • parseResponseBody(response: AppleVerifyReceiptResponseBodySuccess): ParsedResult
  • If you want to perform the HTTP request yourself and just parse the verified receipt, use this function.

    Parameters

    • response: AppleVerifyReceiptResponseBodySuccess

    Returns ParsedResult

Const parseServerToServerNotification

  • If you want to perform the HTTP request yourself and just parse the verified receipt, use this function.

    Warning: This function does not have error handling involved when you pass in an object that is not from Apple. It's assumed you will only ever pass in an object from Apple.

    Parameters

    Returns ParseResult

Const parseSubscriptions

  • parseSubscriptions(latestReceiptInfo?: AppleLatestReceiptInfo[], pendingRenewalInfo?: ApplePendingRenewalInfo[]): AutoRenewableSubscription[]

Const parseSuccess

  • parseSuccess(response: AppleServerNotificationResponseBody): ParsedResult
  • parseSuccess(response: AppleVerifyReceiptResponseBodySuccess): ParsedResult

Const runVerifyReceipt

Const verifyReceipt

  • Verify an in-app purchase receipt. This is a major feature of this library.

    This function will send the receipt to Apple's server and parse the result into something that is much more useful for you, the developer. This allow you to implement in-app purchases faster in your app with server-side verification.

    Parameters

    Returns Promise<ParseResult>

Object literals

Const _

_: object

array

array: object

contains

  • contains<T>(array: T[], doesContain: (element: T) => boolean): boolean
  • Type parameters

    • T

    Parameters

    • array: T[]
    • doesContain: (element: T) => boolean
        • (element: T): boolean
        • Parameters

          • element: T

          Returns boolean

    Returns boolean

date

date: object

sortNewToOld

  • sortNewToOld(first: Date, second: Date): number
  • Sort date array in order: [newer date, older date].

    Use:

    const dates: Date[] = []
    dates.sort(_.date.sortNewToOld)
    
    const nestedDates: {date: Date}[] = []
    nestedDates.sort((first, second) => _.date.sortNewToOld(first.date, second.date))

    Parameters

    • first: Date
    • second: Date

    Returns number

sortOldToNew

  • sortOldToNew(first: Date, second: Date): number
  • Sort date array in order: [older date, newer date].

    Use:

    const dates: Date[] = []
    dates.sort(_.date.sortOldToNew)
    
    const nestedDates: {date: Date}[] = []
    nestedDates.sort((first, second) => _.date.sortOldToNew(first.date, second.date))

    Parameters

    • first: Date
    • second: Date

    Returns number

Legend

  • Constructor
  • Property
  • Method
  • Static property
  • Static method
  • Property
  • Inherited property

Generated using TypeDoc