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)
The year-end shopping spree is around the corner! Is your online store ready for the ...
By JasonH Nov 10, 2024We recently spoke with Zopi developers @Zopi about how dropshipping businesses can enha...
By JasonH Oct 23, 2024A big shout out to all of the merchants who participated in our AMA with 2H Media: Holi...
By Jacqui Oct 21, 2024