shopify-app-template-node
shopify-app-template-node copied to clipboard
Trial Days in ensure-billing.js helper
Issue summary
Expect the ensure billing helper to include a trialDays param as per the mutation docs:- https://shopify.dev/api/admin-graphql/2021-10/mutations/appsubscriptioncreate
Expected behavior
As above
Actual behavior
Current tiralDays if set in index.js example code, is ignored as not present further through the helper methods
Steps to reproduce the problem
Not applicable
I believe I have a solution by adding the trialDays param through the file
import { Shopify } from "@shopify/shopify-api";
export const BillingInterval = {
OneTime: "ONE_TIME",
Every30Days: "EVERY_30_DAYS",
Annual: "ANNUAL",
};
const RECURRING_INTERVALS = [
BillingInterval.Every30Days,
BillingInterval.Annual,
];
let isProd;
/**
* You may want to charge merchants for using your app. This helper provides that function by checking if the current
* merchant has an active one-time payment or subscription named `chargeName`. If no payment is found,
* this helper requests it and returns a confirmation URL so that the merchant can approve the purchase.
*
* Learn more about billing in our documentation: https://shopify.dev/apps/billing
*/
export default async function ensureBilling(
session,
{ chargeName, amount, currencyCode, interval, trialDays },
isProdOverride = process.env.NODE_ENV === "production"
) {
if (!Object.values(BillingInterval).includes(interval)) {
throw `Unrecognized billing interval '${interval}'`;
}
isProd = isProdOverride;
let hasPayment;
let confirmationUrl = null;
if (await hasActivePayment(session, { chargeName, interval })) {
hasPayment = true;
} else {
hasPayment = false;
confirmationUrl = await requestPayment(session, {
chargeName,
amount,
currencyCode,
interval,
trialDays
});
}
return [hasPayment, confirmationUrl];
}
async function hasActivePayment(session, { chargeName, interval }) {
const client = new Shopify.Clients.Graphql(session.shop, session.accessToken);
if (isRecurring(interval)) {
const currentInstallations = await client.query({
data: RECURRING_PURCHASES_QUERY,
});
const subscriptions =
currentInstallations.body.data.currentAppInstallation.activeSubscriptions;
for (let i = 0, len = subscriptions.length; i < len; i++) {
if (
subscriptions[i].name === chargeName &&
(!isProd || !subscriptions[i].test)
) {
return true;
}
}
} else {
let purchases;
let endCursor = null;
do {
const currentInstallations = await client.query({
data: {
query: ONE_TIME_PURCHASES_QUERY,
variables: { endCursor },
},
});
purchases =
currentInstallations.body.data.currentAppInstallation.oneTimePurchases;
for (let i = 0, len = purchases.edges.length; i < len; i++) {
const node = purchases.edges[i].node;
if (
node.name === chargeName &&
(!isProd || !node.test) &&
node.status === "ACTIVE"
) {
return true;
}
}
endCursor = purchases.pageInfo.endCursor;
} while (purchases.pageInfo.hasNextPage);
}
return false;
}
async function requestPayment(
session,
{ chargeName, amount, currencyCode, interval, trialDays }
) {
const client = new Shopify.Clients.Graphql(session.shop, session.accessToken);
const returnUrl = `https://${Shopify.Context.HOST_NAME}?shop=${
session.shop
}&host=${Buffer.from(`${session.shop}/admin`).toString('base64')}`;
let data;
if (isRecurring(interval)) {
const mutationResponse = await requestRecurringPayment(client, returnUrl, {
chargeName,
amount,
currencyCode,
interval,
trialDays
});
data = mutationResponse.body.data.appSubscriptionCreate;
} else {
const mutationResponse = await requestSinglePayment(client, returnUrl, {
chargeName,
amount,
currencyCode,
});
data = mutationResponse.body.data.appPurchaseOneTimeCreate;
}
if (data.userErrors.length) {
throw new ShopifyBillingError(
"Error while billing the store",
data.userErrors
);
}
return data.confirmationUrl;
}
async function requestRecurringPayment(
client,
returnUrl,
{ chargeName, amount, currencyCode, interval, trialDays }
) {
const mutationResponse = await client.query({
data: {
query: RECURRING_PURCHASE_MUTATION,
variables: {
name: chargeName,
lineItems: [
{
plan: {
appRecurringPricingDetails: {
interval,
price: { amount, currencyCode },
},
},
},
],
returnUrl,
test: !isProd,
trialDays,
},
},
});
if (mutationResponse.body.errors && mutationResponse.body.errors.length) {
throw new ShopifyBillingError(
"Error while billing the store",
mutationResponse.body.errors
);
}
return mutationResponse;
}
async function requestSinglePayment(
client,
returnUrl,
{ chargeName, amount, currencyCode }
) {
const mutationResponse = await client.query({
data: {
query: ONE_TIME_PURCHASE_MUTATION,
variables: {
name: chargeName,
price: { amount, currencyCode },
returnUrl,
test: process.env.NODE_ENV !== "production",
},
},
});
if (mutationResponse.body.errors && mutationResponse.body.errors.length) {
throw new ShopifyBillingError(
"Error while billing the store",
mutationResponse.body.errors
);
}
return mutationResponse;
}
function isRecurring(interval) {
return RECURRING_INTERVALS.includes(interval);
}
export function ShopifyBillingError(message, errorData) {
this.name = "ShopifyBillingError";
this.stack = new Error().stack;
this.message = message;
this.errorData = errorData;
}
ShopifyBillingError.prototype = new Error();
const RECURRING_PURCHASES_QUERY = `
query appSubscription {
currentAppInstallation {
activeSubscriptions {
name, test
}
}
}
`;
const ONE_TIME_PURCHASES_QUERY = `
query appPurchases($endCursor: String) {
currentAppInstallation {
oneTimePurchases(first: 250, sortKey: CREATED_AT, after: $endCursor) {
edges {
node {
name, test, status
}
}
pageInfo {
hasNextPage, endCursor
}
}
}
}
`;
const RECURRING_PURCHASE_MUTATION = `
mutation test(
$name: String!
$lineItems: [AppSubscriptionLineItemInput!]!
$returnUrl: URL!
$test: Boolean
$trialDays: Int
) {
appSubscriptionCreate(
name: $name
lineItems: $lineItems
returnUrl: $returnUrl
test: $test
trialDays: $trialDays
) {
confirmationUrl
userErrors {
field
message
}
}
}
`;
const ONE_TIME_PURCHASE_MUTATION = `
mutation test(
$name: String!
$price: MoneyInput!
$returnUrl: URL!
$test: Boolean
) {
appPurchaseOneTimeCreate(
name: $name
price: $price
returnUrl: $returnUrl
test: $test
) {
confirmationUrl
userErrors {
field
message
}
}
}
`;
Then in index.js you would simply pass the num days as an Int:-
const BILLING_SETTINGS = {
required: true,
// This is an example configuration that would do a one-time charge for $5 (only USD is currently supported)
chargeName: "Monthly Plan",
amount: 5.0,
currencyCode: "USD",
trialDays: 7,
interval: BillingInterval.Every30Days,
};```
This issue is stale because it has been open for 60 days with no activity. It will be closed if no further action occurs in 14 days.
We are closing this issue because it has been inactive for a few months. This probably means that it is not reproducible or it has been fixed in a newer version. If it’s an enhancement and hasn’t been taken on since it was submitted, then it seems other issues have taken priority.
If you still encounter this issue with the latest stable version, please reopen using the issue template. You can also contribute directly by submitting a pull request– see the CONTRIBUTING.md file for guidelines
Thank you!