Skip to content

Commit

Permalink
feat: build module for validating receipts
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian committed Nov 30, 2020
1 parent f3590e4 commit d11b9ca
Show file tree
Hide file tree
Showing 86 changed files with 12,366 additions and 4,953 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"files": ["*.test.ts"],
"rules": {
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off"
}
}
]
Expand Down
16 changes: 16 additions & 0 deletions .github/ISSUE_TEMPLATE/BUG_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: 🐛 Bug Report
about: If something isn't working as expected
---

<!-- Thanks for using this project! Before you submit your issue, please make sure you followed our checklist and check the appropriate boxes by putting an x in the [ ]: [x] -->

### New Issue Checklist

- [ ] What version are you using? (Run `npm ls dollabill-apple` to get it):
- [ ] I read [the documentation](https://github.com/levibostian/dollabill-apple/)
- [ ] I searched for [existing GitHub issues](https://github.com/levibostian/dollabill-apple/issues)

### Issue Description

<!-- Please include what's happening, expected behavior, and any relevant code samples. The more you include, the better help you will get back. -->
16 changes: 16 additions & 0 deletions .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: 💡 Feature request
about: If you have an idea or something to improve
---

<!-- Thanks for using this project! Before you submit your issue, please make sure you followed our checklist and check the appropriate boxes by putting an x in the [ ]: [x] -->

### New Issue Checklist

- [ ] What version are you using? (Run `npm ls dollabill-apple` to get it):
- [ ] I read [the documentation](https://github.com/levibostian/dollabill-apple/) to see if the project can already do what I am suggesting.
- [ ] I searched for [existing GitHub issues](https://github.com/levibostian/dollabill-apple/issues) to see if anyone else had the same idea.

### What is the feature request you had in mind?

<!-- Please include what you are using this project to do. Explain your use cases and the problems you are facing. The more you include, the better help you will get back. -->
16 changes: 16 additions & 0 deletions .github/ISSUE_TEMPLATE/HELP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: 😄 Help
about: You have a question about the project
---

<!-- Thanks for using this project! Before you submit your issue, please make sure you followed our checklist and check the appropriate boxes by putting an x in the [ ]: [x] -->

### New Issue Checklist

- [ ] What version are you using? (Run `npm ls dollabill-apple` to get it):
- [ ] I read [the documentation](https://github.com/levibostian/dollabill-apple/) to see if it could answer my question.
- [ ] I searched for [existing GitHub issues](https://github.com/levibostian/dollabill-apple/issues) to see if anyone else has asked the same question.

### What's your question 😄

<!-- Please include what's happening, expected behavior, and any relevant code samples. The more you include, the better help you will get back. -->
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
blank_issues_enabled: false
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ jobs:
- name: Show what will be deployed
run: npm run build && npm publish dist --dry-run
- name: Run tests
run: npm run test && npx codecov
run: npm run test && npx codecov
- name: Compile example project
run: cd example; npm run build; cd ..
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
coverage/
dist/
secrets.json
junit.xml

# Logs
logs
Expand Down
15 changes: 15 additions & 0 deletions @types/tiny-json-http/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
declare type Options = {
url: string
data?: any // form vars for tiny.post, tiny.put, tiny.patch, and tiny.delete otherwise querystring vars for tiny.get
headers?: any // key/value map used for headers (including support for uploading files with multipart/form-data)
buffer?: boolean // if set to true the response body is returned as a buffer
timeout?: number
}

declare type Callback = (err?: Error, res: { headers: any; body: any }) => void

export declare function post(options: Options): Promise<any>
export declare function post(options: Options, callback: Callback): void

export declare function get(options: Options): Promise<any>
export declare function get(options: Options, callback: Callback): void
107 changes: 101 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,104 @@
# npm-module-blanky
# dolla bill

Opinionated boilerplate used to make and deploy npm modules.
Easily work with Apple in-app purchases. So you can more easily collect your "Dolla dolla bills, ya'll".

# Goals of this project
# What is dolla bill?

* Contain configuration files to setup all tools I tend to use in my development flow.
* Clone, rename some files, and get developing!
* Start with zero dependencies. I try my best to keep all npm modules as slim as possible.
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 validation 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.
* Process errors and returns back a helpful error message to you, the developer, to help you fix the problem.
* Process successful responses into a very convenient set of Objects that make working with in-app purchases *finally easy*. We do all of the processing for you.

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...
```ts
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...
```ts
dollaBillResponse.autoRenewableSubscriptions[0].isEligibleIntroductoryOffer
```

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

Not only that, but dolla bill has some great test coverage 😄: [![codecov](https://codecov.io/gh/levibostian/dollabill-apple/branch/master/graph/badge.svg?token=aVY76EstWN)](https://codecov.io/gh/levibostian/dollabill-apple)

# 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.

- [x] **No dependencies**. This module tries to be as small as possible.
- [x] **Fully tested**. Automated testing (with high test coverage) + QA testing gives you the confidence that the module will work well for you.
- [x] **Complete API documentation** to more easily use it in your code.
- [x] **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.
- [x] **Typescript typings** This project is written in Typescript for that strict typing goodness.

> Note: Technically there is [one dependency](https://github.com/brianleroux/tiny-json-http) but it's a _tiny wrapper around a built-in nodejs feature_. This module still remains super small with this.
# Getting started

```ts
import dollaBill from "dollabill"

const receiptFromStoreKit = // The base64 encoded receipt data that StoreKit gave your app

const appleResponse = await dollaBill.verifyReceipt({
appPassword: process.env.APPLE_IN_APP_PURCHASE_PASSWORD,
receipt: receiptFromStoreKit
})

// If an error happens, it's fatal. It means that there is a bug with this library (create a GitHub issue with stacktrace and other info, please) or you the developer made a mistake when using this library.

// It's recommended that you, the developer, views the error message and stacktrace to fix the issue. Note: The error here is not meant to be shown to your users.

if (!appleResponse.isValid) {
// There was a problem. The request was valid and can be retried.

// look at the error and act on it however you wish.
// It's recommended that you log the error and then return back a message to your users saying there was a problem. The error here is meant for the developer to see, not a good error to return back to the user.
appleResponse.error
} else {
// 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.
}
```

# Documentation

// TODO

## Contributors

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key))

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/levibostian"><img src="https://avatars1.githubusercontent.com/u/2041082?v=4" width="100px;" alt=""/><br /><sub><b>Levi Bostian</b></sub></a><br /><a href="https://github.com/levibostian/Purslane/commits?author=levibostian" title="Code">💻</a> <a href="https://github.com/levibostian/Purslane/commits?author=levibostian" title="Documentation">📖</a> <a href="#maintenance-levibostian" title="Maintenance">🚧</a></td>
</tr>
</table>

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->
47 changes: 47 additions & 0 deletions app/_.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { AppleLatestReceiptInfo } from "./verify_receipt/apple_response"
import { AutoRenewableSubscriptionTransaction } from "./verify_receipt/parse/result"

/**
* Global jest setup/teardown functions
*/
Expand All @@ -13,3 +16,47 @@ afterAll(async () => {
afterEach(async () => {
// after each
})

export const parsedTransaction: AutoRenewableSubscriptionTransaction = {
productId: "product-id",
cancelledDate: undefined,
refundReason: undefined,
expiresDate: new Date('2020-11-12T17:43:50-08:00'),
inIntroPeriod: false,
inTrialPeriod: false,
isUpgradeTransaction: false,
offerCodeRefName: undefined,
originalPurchaseDate: new Date("2020-10-12T17:43:50-08:00"),
originalTransactionId: "0000001101010",
promotionalOfferId: undefined,
purchaseDate: new Date("2020-10-12T17:43:50-08:00"),
subscriptionGroupId: "group1",
transactionId: "230403030",
webOrderLineItemId: undefined,
isRenewOrRestore: true,
isFirstSubscriptionPurchase: false,
rawTransaction: ({} as AppleLatestReceiptInfo) // i'm lazy. not used for tests so just leave it empty.
}

export const parsedTransaction2: AutoRenewableSubscriptionTransaction = {
productId: "product-id",
cancelledDate: undefined,
refundReason: undefined,
expiresDate: new Date('2020-09-12T17:43:50-08:00'),
inIntroPeriod: false,
inTrialPeriod: false,
isUpgradeTransaction: false,
offerCodeRefName: undefined,
originalPurchaseDate: new Date("2020-08-12T17:43:50-08:00"),
originalTransactionId: "0000001101010",
promotionalOfferId: undefined,
purchaseDate: new Date("2020-08-12T17:43:50-08:00"),
subscriptionGroupId: "group1",
transactionId: "0000001101010",
webOrderLineItemId: undefined,
isRenewOrRestore: false,
isFirstSubscriptionPurchase: true,
rawTransaction: ({} as AppleLatestReceiptInfo) // i'm lazy. not used for tests so just leave it empty.
}

export default parsedTransaction
Loading

0 comments on commit d11b9ca

Please sign in to comment.