Skip to content

Commit 187a453

Browse files
committed
feat(common): update bobo with new apis
1 parent f5a6721 commit 187a453

File tree

14 files changed

+452
-305
lines changed

14 files changed

+452
-305
lines changed

.env-sample

+15
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,18 @@ MYSQL_DATABASE={mysql database name}
2929
MYSQL_USERNAME={mysql username}
3030
MYSQL_PASSWORD={mysql password}
3131
MYSQL_PORT={mysql port *optional*}
32+
33+
# Partner Billing Config
34+
CURRENT_APP_ID={appId} # if different from CHECKOUT_APP_ID
35+
CHECKOUT_APP_ID={Application ID}
36+
CHECKOUT_CLIENT={Partner Client ID}
37+
CHECKOUT_TOKEN={Partner Access Token}
38+
CHECKOUT_PARTNER={Partner Account UUID}
39+
CHECKOUT_MERCHANT={Merchant Account UUID}
40+
CHECKOUT_URL=https://${API_URL}/accounts/${CHECKOUT_PARTNER}/graphql
41+
42+
# Dev Enironment Vars
43+
ENVIRONMENT={ENV}
44+
LOGIN_URL={login.${ENVIRONMENT}}
45+
API_URL={api.${ENVIRONMENT}}
46+

components/upgradeCard.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ export const UpgradeCard = ({ description, name, pid, popular, price }: UpgradeC
2222
const handleChoosePlan = async () => {
2323
setIsLoading(true);
2424

25+
// TODO: Add error handling
2526
const response = await fetch(`/api/checkout/${pid}?context=${encodedContext}`);
26-
const { checkout_url: url = '' } = await response.json();
27+
const { checkoutUrl: url = '' } = await response.json();
2728

2829
if (url && window) window.location.assign(url);
2930
};
@@ -78,7 +79,7 @@ export const UpgradeCard = ({ description, name, pid, popular, price }: UpgradeC
7879
};
7980

8081
const StyledCard = styled(Box)<BoxProps>`
81-
width: 17.25rem;
82+
width: 18.5rem;
8283
`;
8384

8485
const StyledFlexItem = styled(FlexItem)<FlexItemProps>`

lib/auth.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ export async function getSubscriptionById(pid: string) {
8686
}
8787

8888
export async function getSubscriptionInfo(storeHash: string) {
89-
const subscriptionPlan = await db.getStorePlan(storeHash);
90-
91-
return subscriptionPlan;
89+
return await db.getStorePlan(storeHash);
9290
}
9391

9492
// JWT functions to sign/ verify 'context' query param from /api/auth||load

lib/checkout.ts

+129-18
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
const appId = process.env.APP_ID;
1+
const checkoutAppId = process.env.CHECKOUT_APP_ID;
2+
const appId = process.env.CURRENT_APP_ID ?? checkoutAppId;
3+
const merchantUuid = process.env.CHECKOUT_MERCHANT;
4+
const environment = process.env.ENVIRONMENT;
5+
const isProd = environment === 'bigcommerce.com';
6+
const hostName = isProd ? environment : `-${environment}`;
27

38
export const trialDays = 7;
49

510
export const plans = [
611
{
712
name: 'Standard',
8-
price: '29.95',
13+
price: '40.00',
914
description: `Start out with inventory tracking, product variant lists,
1015
and more. Perfect for businesses with fewer than 1,000 SKUs.`,
1116
popular: false,
@@ -23,41 +28,147 @@ export const plans = [
2328

2429
// TODO: place data inside of a DB and create an API call
2530
const planItems = {
26-
'2': {
31+
'1': {
2732
product: {
28-
id: '2',
33+
id: '1',
2934
type: 'app',
3035
},
31-
pricing_plan: {
32-
interval: 'monthly',
36+
pricingPlan: {
37+
interval: "MONTH",
3338
price: {
34-
amount: 79.95,
35-
currency_code: 'USD',
39+
value: 40.00,
40+
currencyCode: 'USD'
3641
},
3742
},
3843
trial_days: trialDays,
3944
},
40-
'1': {
45+
'2': {
4146
product: {
42-
id: '1',
47+
id: '2',
4348
type: 'app',
4449
},
45-
pricing_plan: {
46-
interval: 'monthly',
50+
pricingPlan: {
51+
interval: 'MONTH',
4752
price: {
48-
amount: 29.95,
49-
currency_code: 'USD'
53+
value: 79.95,
54+
currencyCode: 'USD',
5055
},
5156
},
5257
trial_days: trialDays,
5358
},
5459
};
5560

61+
const checkoutGraphQuery = `
62+
mutation ($checkout: CreateCheckoutInput!) {
63+
checkout {
64+
createCheckout(input: $checkout) {
65+
checkout {
66+
id
67+
accountId
68+
status
69+
checkoutUrl
70+
items(first: 2) {
71+
edges {
72+
node {
73+
subscriptionId
74+
status
75+
product {
76+
entityId
77+
type
78+
}
79+
scope {
80+
entityId
81+
type
82+
}
83+
pricingPlan {
84+
interval
85+
price {
86+
value
87+
currencyCode
88+
}
89+
trialDays
90+
}
91+
redirectUrl
92+
description
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
}
100+
`;
101+
56102
export function getCheckoutBody(productId: string, storeHash: string) {
57103
return {
58-
account_id: '3d12cf3b-1c2a-4731-8049-730d5dee8ed8', // TODO: replace with actual auth
59-
line_items: [planItems[productId]],
60-
notification_url: 'https://8eb69c8ea36a9d3eafe7f05625577609.m.pipedream.net',
61-
redirect_url: `https://store-${storeHash}.mybigcommerce.com/manage/app/${appId}/upgrade/${productId}`,
104+
query: checkoutGraphQuery,
105+
variables: {
106+
checkout: {
107+
accountId: `bc/account/account/${merchantUuid}`,
108+
items: [
109+
{
110+
product: {
111+
entityId: `${checkoutAppId}`,
112+
type: 'APPLICATION'
113+
},
114+
scope: {
115+
entityId: `${storeHash}`,
116+
type: 'STORE'
117+
},
118+
pricingPlan: planItems[productId].pricingPlan,
119+
redirectUrl: `https://store-${storeHash}.my${hostName}/manage/app/${appId}/${productId}`,
120+
description: 'application'
121+
}
122+
]
123+
}
124+
},
125+
};
126+
}
127+
128+
const subscriptionQuery = `
129+
query q1($id: ID!) {
130+
account {
131+
checkout(id: $id) {
132+
id
133+
accountId
134+
status
135+
checkoutUrl
136+
items(first: 2) {
137+
edges {
138+
node {
139+
subscriptionId
140+
status
141+
product {
142+
entityId
143+
type
144+
}
145+
scope {
146+
entityId
147+
type
148+
}
149+
pricingPlan {
150+
interval
151+
price {
152+
value
153+
currencyCode
154+
}
155+
trialDays
156+
}
157+
redirectUrl
158+
description
159+
}
160+
}
161+
}
162+
}
163+
}
164+
}
165+
`;
166+
167+
export function getSubscriptionBody(subscriptionId: string) {
168+
return {
169+
query: subscriptionQuery,
170+
variables: {
171+
"id": `bc/account/checkout/${subscriptionId}`
172+
}
62173
};
63174
}

lib/dbs/firebase.ts

+16-16
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ const db = getFirestore(app);
2020
export async function setSubscriptionId(pid: string, subscriptionId: string) {
2121
if (!pid || !subscriptionId) return null;
2222

23-
const ref = db.collection('subscription').doc(pid);
24-
await ref.set({ subscriptionId });
23+
const ref = doc(db, 'subscription', pid);
24+
25+
await setDoc(ref, { subscriptionId });
2526
}
2627

2728
// Use setUser for storing global user data (persists between installs)
@@ -63,22 +64,19 @@ export async function setStorePlan(session: SessionProps) {
6364

6465
const contextString = context ?? sub;
6566
const storeHash = contextString?.split('/')[1] || '';
66-
67-
if (!plan?.pid && await getStorePlan(storeHash)) return null; // Return early if set
68-
6967
const defaultEnd = Date.now() + (trialDays * 24 * 60 * 60 * 1000);
70-
const { pid = '', isPaidApp = false, showPaidWelcome = false, trialEndDate = defaultEnd } = plan ?? {};
71-
const ref = db.collection('plan').doc(storeHash);
72-
const data = { pid, isPaidApp, showPaidWelcome, trialEndDate };
68+
const ref = doc(db, 'plan', storeHash);
69+
const data = { pid: '', isPaidApp: false, showPaidWelcome: false, trialEndDate: defaultEnd, ...plan };
7370

74-
await ref.set(data);
71+
await setDoc(ref, data);
7572
}
7673

7774
export async function setStoreWelcome(storeHash: string, show: boolean) {
7875
if (!storeHash) return null;
79-
const ref = db.collection('plan').doc(storeHash);
8076

81-
await ref.set({ showPaidWelcome: show }, { merge: true });
77+
const ref = doc(db, 'plan', storeHash);
78+
79+
await setDoc(ref, { showPaidWelcome: show }, { merge: true });
8280
}
8381

8482
// User management for multi-user apps
@@ -138,23 +136,25 @@ export async function hasStoreUser(storeHash: string, userId: string) {
138136
export async function getStorePlan(storeHash: string) {
139137
if (!storeHash) return null;
140138

141-
const doc = await db.collection('plan').doc(storeHash).get();
139+
const planDoc = await getDoc(doc(db, 'plan', storeHash));
142140

143-
return doc?.exists ? doc.data() : null;
141+
return planDoc.exists() ? planDoc.data() : null;
144142
}
145143

146144
export async function getStoreToken(storeHash: string) {
147145
if (!storeHash) return null;
146+
148147
const storeDoc = await getDoc(doc(db, 'store', storeHash));
149148

150-
return storeDoc.data()?.accessToken ?? null;
149+
return storeDoc.exists() ? storeDoc.data()?.accessToken : null;
151150
}
152151

153152
export async function getSubscriptionId(pid: string) {
154153
if (!pid) return null;
155-
const doc = await db.collection('subscription').doc(pid).get();
156154

157-
return doc?.exists ? doc.data()?.subscriptionId : null;
155+
const subDoc = await getDoc(doc(db, 'subscription', pid));
156+
157+
return subDoc.exists() ? subDoc.data()?.subscriptionId : null;
158158
}
159159

160160
export async function deleteStore({ store_hash: storeHash }: SessionProps) {

lib/hooks.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ export function useAlerts() {
3333
}
3434

3535
export function useSubscription() {
36-
const encodedContext = useSession()?.context;
37-
const { data, error } = useSWR(encodedContext ? ['/api/subscription', encodedContext] : null, fetcher);
36+
const { context } = useSession();
37+
const params = new URLSearchParams({ context }).toString();
38+
const { data, error } = useSWR(context ? ['/api/subscription', params] : null, fetcher);
3839

3940
return {
4041
subscription: data,

pages/api/checkout/[pid].ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,26 @@ export default async function checkout(req: NextApiRequest, res: NextApiResponse
88
try {
99
const { storeHash } = await getSession(req);
1010
const checkoutBody = getCheckoutBody(String(pid), storeHash);
11-
const response = await fetch(`${process.env.CHECKOUT_URL}/checkouts`, {
11+
const response = await fetch(process.env.CHECKOUT_URL, {
1212
method: 'POST',
1313
headers: {
14-
'Content-Type': 'application/json',
15-
'X-Auth-Token': process.env.CHECKOUT_TOKEN,
16-
'X-Partner-ID': process.env.CHECKOUT_PARTNER,
14+
'Accept': 'application/json',
15+
'Content-Type': 'application/json',
16+
'X-Auth-Token': process.env.CHECKOUT_TOKEN,
17+
'X-Auth-Client': process.env.CHECKOUT_CLIENT,
1718
},
18-
body: JSON.stringify(checkoutBody)
19+
body: JSON.stringify(checkoutBody),
1920
});
20-
const { data } = await response.json();
21-
const subId = data?.line_items?.[0]?.subscription_id ?? '';
21+
const { data, errors } = await response.json();
2222

23+
if (data === null && errors[0]?.message ) {
24+
throw new Error(errors[0]?.message);
25+
}
26+
27+
const subId = data?.checkout?.createCheckout?.checkout?.id.split('/')[3] ?? '';
2328
if (subId) setSubscription(String(pid), subId);
2429

25-
res.status(200).json(data);
30+
res.status(200).json(data?.checkout?.createCheckout?.checkout);
2631
} catch (error) {
2732
const { message, response } = error;
2833
res.status(response?.status || 500).json({ message });

pages/api/load.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
22
import { encodePayload, getBCVerify, getSubscriptionById, setSession } from '../../lib/auth';
3+
import { getSubscriptionBody } from '../../lib/checkout';
34

45
const buildRedirectUrl = (url: string, encodedContext: string) => {
56
const [path, query = ''] = url.split('?');
@@ -15,19 +16,27 @@ export default async function load(req: NextApiRequest, res: NextApiResponse) {
1516
const { url } = session;
1617
const [,,pid] = url.match(/(\/upgrade)\/([0-9]+)/) ?? [];
1718

19+
// If redirected from checkout, verify checkout
1820
if (pid) {
19-
const subId = await getSubscriptionById(pid) ?? '';
20-
if (subId) {
21-
const response = await fetch(`${process.env.CHECKOUT_URL}/subscriptions?subscriptionId=${subId}`, {
21+
const subId = await getSubscriptionById(pid);
22+
if (subId !== null) {
23+
const subscriptionBody = getSubscriptionBody(String(subId));
24+
const response = await fetch(`${process.env.CHECKOUT_URL}`, {
25+
method: 'POST',
2226
headers: {
23-
'X-Auth-Token': process.env.CHECKOUT_TOKEN,
24-
'X-Partner-ID': process.env.CHECKOUT_PARTNER,
25-
}
27+
'Accept': 'application/json',
28+
'Content-Type': 'application/json',
29+
'X-Auth-Token': process.env.CHECKOUT_TOKEN,
30+
'X-Auth-Client': process.env.CHECKOUT_CLIENT,
31+
},
32+
body: JSON.stringify(subscriptionBody),
2633
});
27-
const { data: [{ status = '', trial_ends_on: trialEndDate = Date.now() }] = [] } = await response.json();
28-
const isPaidApp = status === 'active';
34+
const { data: { account: { checkout: { status = '', items = {} } = {} } = {} } = {} } = await response.json();
35+
const trialEndDate = items?.edges?.[0]?.node?.pricingPlan?.trialDays ?? Date.now();
36+
const isPaidApp = status === 'COMPLETE';
2937

3038
session.plan = { pid, isPaidApp, showPaidWelcome: isPaidApp, trialEndDate };
39+
session.url = '/';
3140
}
3241
}
3342
const encodedContext = encodePayload(session); // Signed JWT to validate/ prevent tampering

0 commit comments

Comments
 (0)