App reviews, troubleshooting, and recommendations
How to redirect Shopify embedded to shopify billing api confirmation URL.
Redirect is not working and window.location.href also not working.
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>
```
```
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. 🙂
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.
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!
The "free" plan is just no plan in our app. So if the user has no subscription he is on a "free plan".
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.
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 🙏🏼
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.
window.location does not work here please use window.top.location.replace(yourUrl)
Starting a B2B store is a big undertaking that requires careful planning and execution. W...
By JasonH Sep 23, 2024By investing 30 minutes of your time, you can unlock the potential for increased sales,...
By Jacqui Sep 11, 2024We appreciate the diverse ways you participate in and engage with the Shopify Communi...
By JasonH Sep 9, 2024