Re: Shopify App Billing API (Remix)

Why isn't my Shopify App Billing API redirect working?

skn1
Shopify Partner
7 0 1

How to redirect Shopify embedded to shopify billing api confirmation URL.


import { redirect } from "@remix-run/node";


Redirect is not working and window.location.href also not working.

Skn
Replies 9 (9)

frknue
Shopify Partner
5 0 1

Something like this should be working.

```

export const action: ActionFunction = async ({ request }) => {
const { billing } = await authenticate.admin(request);
await billing.require({
plans: [BASIC_TIER],
isTest: true,
onFailure: async () => billing.request({ plan: FREE_TIER }),
});

return null;
};

```
<Card>
<Form method="post">
<Button submit>Subscribe</Button>
</Form>
</Card>

```


```

felixmpaulus
Shopify Partner
55 0 19

How would you require billing for a free plan? I thought a plan must have a price >0? Please provide more code if possible, such as the billingConfig. 🙂

Add bullet points to your productpage with Bloom: Product Feature Bullets
Increase branding and conversion. Set your store apart.
❤️ Free Plan available. Exclusively 5-star reviews.
frknue
Shopify Partner
5 0 1

My bad as you said, the amount for a billing plan has to more than zero. Here is an example for one of our Apps.

shopify.server.ts
```
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || '',
apiVersion: LATEST_API_VERSION,
scopes: process.env.SCOPES?.split(','),
appUrl: process.env.SHOPIFY_APP_URL || '',
authPathPrefix: '/auth',
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
restResources,
useOnlineTokens: true,
billing: {
[BASIC_TIER]: {
amount: 9.95,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
},
[PREMIUM_TIER]: {
amount: 39.95,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
},
[ULTIMATE_TIER]: {
amount: 99.95,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
},
},
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks',
},
},
hooks: {
afterAuth: async ({ session }) => {
shopify.registerWebhooks({ session });
},
},
...(process.env.SHOP_CUSTOM_DOMAIN
? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
: {}),
});

```
Billing logic example:

```

let PLAN: any;
switch (id) {
case '2':
PLAN = BASIC_TIER;
break;
case '3':
PLAN = PREMIUM_TIER;
break;
case '4':
PLAN = ULTIMATE_TIER;
break;
default:
return json({ message: 'Invalid tier' }, { status: 400 });
}

await billing.require({
plans: [PLAN],
onFailure: async () => billing.request({ plan: PLAN }),
});

```
This will redirect the user to the Shopify billing page, if there is no active subscription.

felixmpaulus
Shopify Partner
55 0 19

Thank you for providing the code! 🙂

Unfortunately this does not help me with how to handle a free plan. 

I created this issue https://github.com/Shopify/shopify-app-template-remix/issues/431.

 

I would really appreciate any input. Thank you!

Add bullet points to your productpage with Bloom: Product Feature Bullets
Increase branding and conversion. Set your store apart.
❤️ Free Plan available. Exclusively 5-star reviews.
frknue
Shopify Partner
5 0 1

The "free" plan is just no plan in our app. So if the user has no subscription he is on a "free plan".

frknue
Shopify Partner
5 0 1

For example this is a workaround to get the information of the billing for the current user. The onFailure callback does nothing.
```

let billingInfo;

try {
billingInfo = await billing.require({
plans: [BASIC_TIER, PREMIUM_TIER, ULTIMATE_TIER],
isTest: false,
onFailure: async (error) => {
return new Response(null, { status: 204 }); // No Content response
},
});
} catch (error) {
// Handle any unexpected errors here
}
```
With `billingInfo` you can handle the logic.

felixmpaulus
Shopify Partner
55 0 19

Thank you so much for you input!

I have tried your code and it always ends up throwing this huge statuscode on my screen and it always ends in an error.

I do not want to handle a failure as an error. I simply want to execute my own logic in case the user has no billing.

 

I am new to Remix so again, any help would be greatly appreciated 🙏🏼

Screenshot 2023-11-24 at 18.03.32.png

Add bullet points to your productpage with Bloom: Product Feature Bullets
Increase branding and conversion. Set your store apart.
❤️ Free Plan available. Exclusively 5-star reviews.
frknue
Shopify Partner
5 0 1

We had the same problem, the `billing.require` function always expects a `onFailure ` fallback. I found a workaround, that's why I pass a new response with null as a parameter. This won't work outside of a try catch block.

Here you can see the full loader function. I hope this helps. Shopify needs to make the onFailure callback obligatory.

export async function loader({ request }) {
const { admin, billing } = await authenticate.admin(request);

let billingInfo;

try {
billingInfo = await billing.require({
plans: [BASIC_TIER, PREMIUM_TIER, ULTIMATE_TIER],
isTest: false,
onFailure: async (error) => {
return new Response(null, { status: 204 }); // No Content response
},
});
} catch (error) {
// Handle any unexpected errors here
}

// use the id to get more information about the plan
let subscriptionInfo: any;

if (billingInfo?.appSubscriptions[0]) {
// Extract the numerical ID from the GraphQL ID
const fullId = billingInfo.appSubscriptions[0].id;
const idParts = fullId.split('/');
const numericalId = idParts[idParts.length - 1];

try {
const response = await admin.rest.get({
path: `recurring_application_charges/${numericalId}.json`,
});

if (response.ok) {
const jsonResponse = await response.json();
subscriptionInfo = jsonResponse.recurring_application_charge;
} else {
console.error('Response not OK:', response.status);
}
} catch (error) {
console.error('Error fetching subscription info:', error);
}
}

const subscriptionPlan = getSubscriptionPlan(billingInfo, subscriptionPlans);

const graphlqlResponse = await admin.graphql(`{shop {currencyCode}}`);

const shopData = await graphlqlResponse.json();

return json({
apiKey: process.env.SHOPIFY_API_KEY,
subscriptionPlan: subscriptionPlan,
subscriptionInfo: subscriptionInfo,
shopCurrency: Currency[shopData.data.shop.currencyCode],
});
}


PS: This is also my first Remix App, if there is a better way to this, I welcome your input and assistance.

iamgurisehgal
Shopify Partner
1 0 0

window.location does not work here please use window.top.location.replace(yourUrl)