Skip to content

Commit

Permalink
Allow not filtering by plans in billing checks
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Jul 22, 2024
1 parent 74fc45a commit 05fb23d
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 119 deletions.
6 changes: 6 additions & 0 deletions .changeset/perfect-countries-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/shopify-app-remix': minor
'@shopify/shopify-api': minor
---

Allow finding charges for apps using [Shopify managed pricing](https://shopify.dev/docs/apps/launch/billing/managed-pricing) by calling the billing `check` function without a `plans` filter.
51 changes: 32 additions & 19 deletions packages/apps/shopify-api/docs/guides/billing.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
# Configuring App Billing

Some partners might wish to charge merchants for using their app.
The Admin API provides endpoints that enable apps to trigger purchases in the Shopify platform, so that merchants can pay for apps as part of their Shopify payments.
Some partners might wish to charge merchants for using their app. Shopify makes it possible to do that in two ways, so that merchants can pay for apps as part of their Shopify payments:

- Using Shopify managed app pricing
- Using the Admin billing API to trigger purchases in the Shopify platform.

In this guide you'll learn how to use this package for both of those scenarios.

## Using managed app pricing

The easiest way to add billing to your app is to follow the [Shopify managed app pricing documentation](https://shopify.dev/docs/apps/launch/billing/managed-pricing). You can set up one or more plans for the app, and Shopify will host a page where merchants can select which plan they want.

Since Shopify handles billing in this scenario, you don't have to add a `billing` setting to your configuration, but you can still use the [`check`](../reference/billing/check.md) method to get the plans for the current merchant.

## Using the billing API

If you'd prefer to have full control over billing, you can use the helpers in this package to interact with the billing API directly.

See the [billing reference](../reference/billing/README.md) for details on how to call those endpoints, using this configuration.

To trigger the billing behaviour, you'll need to set the `billing` value when calling `shopifyApi()`.

## Configuring LineItem billing
### Configuring LineItem billing

With the future flag `v10_lineItemBilling`, the billing configuration can now specify the the `AppSubscriptionLineItems`. This will allow you to create app subscription plans with both recurring and usage based charges.

Subscription plans can have 1 or 2 line items. There can be a maximum of 1 of each type of plan Usage and Recurring. Usage line items can only be used in conjunction with recurring line items when the recurring line item interval is `BillingInterval.Every30Days`.

For configuring billing without line items see [Configuring Billing](#configuring-billing).

### Configuring a Subscription Plan with a Single LineItem
#### Configuring a Subscription Plan with a Single LineItem

```ts
import {
shopifyApi,
Expand Down Expand Up @@ -51,7 +66,8 @@ future: {
});
```
### Configuring a Subscription Plan with Multiple LineItems
#### Configuring a Subscription Plan with Multiple LineItems
```ts
import {
shopifyApi,
Expand Down Expand Up @@ -93,7 +109,8 @@ future: {
});
```
### Configuring a one-time charge
#### Configuring a one-time charge
```ts
import {
shopifyApi,
Expand All @@ -117,16 +134,15 @@ future: {
});
```
### Subscription Plan with LineItems
#### Subscription Plan with LineItems
| Parameter | Type | Required? | Default Value | Notes |
| ----------------------------------- | ---------------------------- | :-------: | :-----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LineItems` | `LineItems[]` | Yes | - | An array of LineItems to be included in the subscription plan. |
| `trialDays` | `number` | No | - | Give merchants this many days before charging |
| `replacementBehavior` | `BillingReplacementBehavior` | No | - | `BillingReplacementBehavior` value, see [the reference](https://shopify.dev/docs/api/admin-graphql/latest/mutations/appSubscriptionCreate) for more information. |
### Recurring Charge LineItem
#### Recurring Charge LineItem
| Parameter | Type | Required? | Default Value | Notes |
| ----------------------------------- | ---------------------------- | :-------: | :-----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -137,7 +153,7 @@ future: {
| `discount.value.amount` | `number` | No | - | The amount of the discount in the currency that the merchant is being billed in. |
| `discount.value.percentage` | `number` | No | - | The percentage value of the discount. |
### Usage Charge LineItem
#### Usage Charge LineItem
| Parameter | Type | Required? | Default Value | Notes |
| --------------------- | ---------------------------- | :-------: | :-----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -146,17 +162,15 @@ future: {
| `currencyCode` | `string` | Yes | - | The currency to charge, USD or merchant's shop currency<sup>1</sup> |
| `usageTerms` | `string` | Yes | - | These terms stipulate the pricing model for the charges that an app creates. |
### One Time Billing Plans
#### One Time Billing Plans
| Parameter | Type | Required? | Default Value | Notes |
| -------------- | ---------- | :-------: | :-----------: | ------------------------------------------------------------------- |
| `interval` | `ONE_TIME` | Yes | - | `BillingInterval.OneTime` value |
| `amount` | `number` | Yes | - | The amount to charge |
| `currencyCode` | `string` | Yes | - | The currency to charge, USD or merchant's shop currency<sup>1</sup> |
## Configuring Billing
### Configuring Billing
Prior to the future flag `v10_lineItemBilling` and when the flag is set to false you will configure billing as follows. For example, the following configuration will allow you to charge merchants $30 every 30 days. The first three charges will be discounted by $10, so merchants would be charged $20.
Expand Down Expand Up @@ -186,7 +200,7 @@ const shopify = shopifyApi({
});
```
### Recurring Billing Plans
#### Recurring Billing Plans
| Parameter | Type | Required? | Default Value | Notes |
| ----------------------------------- | ---------------------------- | :-------: | :-----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -201,8 +215,7 @@ const shopify = shopifyApi({
> **Note** `discount.value` can only include either `amount` or `percentage` but not both.
### Usage Billing Plans
#### Usage Billing Plans
| Parameter | Type | Required? | Default Value | Notes |
| --------------------- | ---------------------------- | :-------: | :-----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -215,7 +228,7 @@ const shopify = shopifyApi({
1. Prior to `ApiVersion.April23` the currency code must be `USD`.
## When should the app check for payment?
### When should the app check for payment?
As mentioned above, billing requires a session to access the API, which means that the app must actually be installed before it can request payment.
Expand All @@ -227,7 +240,7 @@ If you're gating access to the entire app, you should check for billing:
**Note**: the merchant may refuse payment when prompted or cancel subscriptions later on, but the app will already be installed at that point. We recommend using [billing webhooks](https://shopify.dev/docs/apps/billing#webhooks-for-billing) to revoke access for merchants when they cancel / decline payment.
## Canceling a subscription
### Canceling a subscription
With the `cancel` method you'll be able to cancel a single subscription. First, you'll need to obtain the id for the subscription you wish to cancel. You can use the `subscriptions` method to obtain a list of current subscriptions.
Expand Down
31 changes: 30 additions & 1 deletion packages/apps/shopify-api/docs/reference/billing/check.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@ Checks if a payment exists for any of the given plans, by querying the Shopify A

> **Note**: Depending on the number of requests your app handles, you might want to cache a merchant's payment status, but you should periodically call this method to ensure you're blocking unpaid access.
## Example (using Shopify managed pricing)

```ts
// You can also use a middleware to get the current plan for the user
app.get('/billed-page', (req, res) => {
const sessionId = shopify.session.getCurrentId({
isOnline: true,
rawRequest: req,
rawResponse: res,
});

// use sessionId to retrieve session from app's session storage
// In this example, getSessionFromStorage() must be provided by app
const session = await getSessionFromStorage(sessionId);

const payments = await shopify.billing.check({
session,
isTest: true,
});

// The above will fetch all subscriptions, so you can filter by the plan you want
const hasPlan1 = payments.appSubscriptions.some((subscription) => {
return subscription.name === 'Plan 1';
});

// Return request here, or render a dynamic page based on the plan
});
```

## Example (not using return objects)

```ts
Expand Down Expand Up @@ -120,7 +149,7 @@ The `Session` for the current request.

### plans

`string | string[]` | :exclamation: required
`string | string[]` | :exclamation: required when not using managed pricing

Which plans to look for.

Expand Down
18 changes: 17 additions & 1 deletion packages/apps/shopify-api/lib/billing/__tests__/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('shopify.billing.check', () => {
});

describe('with no billing config', () => {
test('throws error', async () => {
test('throws error if plans are given', async () => {
const shopify = shopifyApi(testConfig({billing: undefined}));

expect(() =>
Expand All @@ -65,6 +65,22 @@ describe('shopify.billing.check', () => {
}),
).rejects.toThrowError(BillingError);
});

test('returns all purchases if no plans are given', async () => {
const shopify = shopifyApi(testConfig({billing: undefined}));

queueMockResponses([Responses.MULTIPLE_SUBSCRIPTIONS]);

const response = await shopify.billing.check({session, isTest: true});

expect({
...GRAPHQL_BASE_REQUEST,
data: expect.stringContaining('activeSubscriptions'),
}).toMatchMadeHttpRequest();

expect(response.oneTimePurchases.length).toBe(0);
expect(response.appSubscriptions.length).toBe(2);
});
});

describe('with non-recurring configs', () => {
Expand Down
15 changes: 15 additions & 0 deletions packages/apps/shopify-api/lib/billing/__tests__/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,21 @@ export const EXISTING_SUBSCRIPTION = JSON.stringify(
EXISTING_SUBSCRIPTION_OBJECT,
);

export const MULTIPLE_SUBSCRIPTIONS = JSON.stringify({
data: {
currentAppInstallation: {
oneTimePurchases: {
edges: [],
pageInfo: {hasNextPage: false, endCursor: null},
},
activeSubscriptions: [
{id: 'gid://123', name: PLAN_1, test: true},
{id: 'gid://321', name: PLAN_2, test: true},
],
},
},
});

export const PURCHASE_ONE_TIME_RESPONSE = JSON.stringify({
data: {
appPurchaseOneTimeCreate: {
Expand Down
Loading

0 comments on commit 05fb23d

Please sign in to comment.