Skip to content

Commit 46b1aa7

Browse files
authored
Merge pull request #128 from bc-chaz/appex-402
fix(common): fixes upgrade alert race condition
2 parents d8c8e1f + 87616c5 commit 46b1aa7

File tree

10 files changed

+94
-81
lines changed

10 files changed

+94
-81
lines changed

.env-sample

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ MYSQL_PASSWORD={mysql password}
3131
MYSQL_PORT={mysql port *optional*}
3232

3333
# Partner Billing Config
34-
CURRENT_APP_ID={appId} # if different from CHECKOUT_APP_ID
34+
CURRENT_APP_ID={appId *if different from CHECKOUT_APP_ID*}
3535
CHECKOUT_APP_ID={Application ID}
3636
CHECKOUT_CLIENT={Partner Client ID}
3737
CHECKOUT_TOKEN={Partner Access Token}

lib/auth.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,6 @@ export function setSession(session: SessionProps) {
5858
db.setStoreUser(session);
5959
}
6060

61-
export function setSubscription(pid: string, subId: string) {
62-
db.setSubscriptionId(pid, subId);
63-
}
64-
65-
export function setWelcome(storeHash: string, show: boolean) {
66-
db.setStoreWelcome(storeHash, show);
67-
}
68-
6961
export async function getSession({ query: { context = '' } }: NextApiRequest) {
7062
if (typeof context !== 'string') return;
7163
const { context: storeHash, plan, user } = decodePayload(context);
@@ -81,14 +73,6 @@ export async function getSession({ query: { context = '' } }: NextApiRequest) {
8173
return { accessToken, ...(plan && { plan }), storeHash, user };
8274
}
8375

84-
export async function getSubscriptionById(pid: string) {
85-
return await db.getSubscriptionId(pid);
86-
}
87-
88-
export async function getSubscriptionInfo(storeHash: string) {
89-
return await db.getStorePlan(storeHash);
90-
}
91-
9276
// JWT functions to sign/ verify 'context' query param from /api/auth||load
9377
export function encodePayload({ user, owner, ...session }: SessionProps) {
9478
const contextString = session?.context ?? session?.sub;
@@ -117,3 +101,20 @@ export async function logoutUser({ storeHash, user }: SessionContextProps) {
117101
const session = { context: `store/${storeHash}`, user };
118102
await db.deleteUser(session);
119103
}
104+
105+
// CHECKOUT functions
106+
export function setCheckout(pid: string, subId: string) {
107+
db.setCheckoutId(pid, subId);
108+
}
109+
110+
export async function getCheckoutById(pid: string) {
111+
return await db.getCheckoutId(pid);
112+
}
113+
114+
export function setWelcome(storeHash: string, show: boolean) {
115+
db.setStoreWelcome(storeHash, show);
116+
}
117+
118+
export async function getSubscriptionInfo(storeHash: string) {
119+
return await db.getStorePlan(storeHash);
120+
}

lib/checkout.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function getCheckoutBody(productId: string, storeHash: string) {
116116
type: 'STORE'
117117
},
118118
pricingPlan: planItems[productId].pricingPlan,
119-
redirectUrl: `https://store-${storeHash}.my${hostName}/manage/app/${appId}/${productId}`,
119+
redirectUrl: `https://store-${storeHash}.my${hostName}/manage/app/${appId}/upgrade/${productId}`,
120120
description: 'application'
121121
}
122122
]

lib/dbs/firebase.ts

+42-42
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,6 @@ const db = getFirestore(app);
1616

1717
// Firestore data management functions
1818

19-
// Persist subscription info
20-
export async function setSubscriptionId(pid: string, subscriptionId: string) {
21-
if (!pid || !subscriptionId) return null;
22-
23-
const ref = doc(db, 'subscription', pid);
24-
25-
await setDoc(ref, { subscriptionId });
26-
}
27-
2819
// Use setUser for storing global user data (persists between installs)
2920
export async function setUser({ user }: SessionProps) {
3021
if (!user) return null;
@@ -57,28 +48,6 @@ export async function setStore(session: SessionProps) {
5748
await setDoc(ref, data);
5849
}
5950

60-
export async function setStorePlan(session: SessionProps) {
61-
const { access_token: accessToken, context, plan, sub } = session;
62-
// Only set on app install or subscription verification (load)
63-
if ((!accessToken && !plan?.pid) || (plan && !plan.isPaidApp)) return null;
64-
65-
const contextString = context ?? sub;
66-
const storeHash = contextString?.split('/')[1] || '';
67-
const defaultEnd = Date.now() + (trialDays * 24 * 60 * 60 * 1000);
68-
const ref = doc(db, 'plan', storeHash);
69-
const data = { pid: '', isPaidApp: false, showPaidWelcome: false, trialEndDate: defaultEnd, ...plan };
70-
71-
await setDoc(ref, data);
72-
}
73-
74-
export async function setStoreWelcome(storeHash: string, show: boolean) {
75-
if (!storeHash) return null;
76-
77-
const ref = doc(db, 'plan', storeHash);
78-
79-
await setDoc(ref, { showPaidWelcome: show }, { merge: true });
80-
}
81-
8251
// User management for multi-user apps
8352
// Use setStoreUser for storing store specific variables
8453
export async function setStoreUser(session: SessionProps) {
@@ -114,7 +83,6 @@ export async function setStoreUser(session: SessionProps) {
11483
}
11584
}
11685

117-
11886
export async function deleteUser({ context, user, sub }: SessionProps) {
11987
const contextString = context ?? sub;
12088
const storeHash = contextString?.split('/')[1] || '';
@@ -133,6 +101,36 @@ export async function hasStoreUser(storeHash: string, userId: string) {
133101
return userDoc.exists();
134102
}
135103

104+
export async function getStoreToken(storeHash: string) {
105+
if (!storeHash) return null;
106+
107+
const storeDoc = await getDoc(doc(db, 'store', storeHash));
108+
109+
return storeDoc.exists() ? storeDoc.data()?.accessToken : null;
110+
}
111+
112+
113+
export async function deleteStore({ store_hash: storeHash }: SessionProps) {
114+
const ref = doc(db, 'store', storeHash);
115+
116+
await deleteDoc(ref);
117+
}
118+
119+
// CHECKOUT Functions
120+
export async function setStorePlan(session: SessionProps) {
121+
const { access_token: accessToken, context, plan, sub } = session;
122+
// Only set on app install or subscription verification (load)
123+
if ((!accessToken && !plan?.pid) || (plan && !plan.isPaidApp)) return null;
124+
125+
const contextString = context ?? sub;
126+
const storeHash = contextString?.split('/')[1] || '';
127+
const defaultEnd = Date.now() + (trialDays * 24 * 60 * 60 * 1000);
128+
const ref = doc(db, 'plan', storeHash);
129+
const data = { pid: '', isPaidApp: false, showPaidWelcome: false, trialEndDate: defaultEnd, ...plan };
130+
131+
await setDoc(ref, data);
132+
}
133+
136134
export async function getStorePlan(storeHash: string) {
137135
if (!storeHash) return null;
138136

@@ -141,24 +139,26 @@ export async function getStorePlan(storeHash: string) {
141139
return planDoc.exists() ? planDoc.data() : null;
142140
}
143141

144-
export async function getStoreToken(storeHash: string) {
142+
export async function setStoreWelcome(storeHash: string, show: boolean) {
145143
if (!storeHash) return null;
146144

147-
const storeDoc = await getDoc(doc(db, 'store', storeHash));
145+
const ref = doc(db, 'plan', storeHash);
148146

149-
return storeDoc.exists() ? storeDoc.data()?.accessToken : null;
147+
await setDoc(ref, { showPaidWelcome: show }, { merge: true });
150148
}
151149

152-
export async function getSubscriptionId(pid: string) {
153-
if (!pid) return null;
150+
export async function setCheckoutId(pid: string, checkoutId: string) {
151+
if (!pid || !checkoutId) return null;
154152

155-
const subDoc = await getDoc(doc(db, 'subscription', pid));
153+
const ref = doc(db, 'checkout', pid);
156154

157-
return subDoc.exists() ? subDoc.data()?.subscriptionId : null;
155+
await setDoc(ref, { checkoutId });
158156
}
159157

160-
export async function deleteStore({ store_hash: storeHash }: SessionProps) {
161-
const ref = doc(db, 'store', storeHash);
158+
export async function getCheckoutId(pid: string) {
159+
if (!pid) return null;
162160

163-
await deleteDoc(ref);
161+
const checkoutDoc = await getDoc(doc(db, 'checkout', pid));
162+
163+
return checkoutDoc.exists() ? checkoutDoc.data()?.checkoutId : null;
164164
}

pages/api/checkout/[pid].ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
2-
import { getSession, setSubscription } from '../../../lib/auth';
2+
import { getSession, setCheckout } from '../../../lib/auth';
33
import { getCheckoutBody } from '../../../lib/checkout';
44

55
export default async function checkout(req: NextApiRequest, res: NextApiResponse) {
@@ -24,8 +24,8 @@ export default async function checkout(req: NextApiRequest, res: NextApiResponse
2424
throw new Error(errors[0]?.message);
2525
}
2626

27-
const subId = data?.checkout?.createCheckout?.checkout?.id.split('/')[3] ?? '';
28-
if (subId) setSubscription(String(pid), subId);
27+
const checkoutId = data?.checkout?.createCheckout?.checkout?.id.split('/')[3] ?? '';
28+
if (checkoutId) setCheckout(String(pid), checkoutId);
2929

3030
res.status(200).json(data?.checkout?.createCheckout?.checkout);
3131
} catch (error) {

pages/api/checkout/removeWelcome.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
import { getSession, setWelcome } from '../../../lib/auth';
3+
4+
export default async function removeWelcome(req: NextApiRequest, res: NextApiResponse) {
5+
try {
6+
const { storeHash } = await getSession(req);
7+
setWelcome(storeHash, false);
8+
9+
res.status(200).json({});
10+
} catch (error) {
11+
const { message, response } = error;
12+
res.status(response?.status || 500).json({ message });
13+
}
14+
}

pages/api/load.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
2-
import { encodePayload, getBCVerify, getSubscriptionById, setSession } from '../../lib/auth';
2+
import { encodePayload, getBCVerify, getCheckoutById, setSession } from '../../lib/auth';
33
import { getSubscriptionBody } from '../../lib/checkout';
44

55
const buildRedirectUrl = (url: string, encodedContext: string) => {
@@ -18,7 +18,7 @@ export default async function load(req: NextApiRequest, res: NextApiResponse) {
1818

1919
// If redirected from checkout, verify checkout
2020
if (pid) {
21-
const subId = await getSubscriptionById(pid);
21+
const subId = await getCheckoutById(pid);
2222
if (subId !== null) {
2323
const subscriptionBody = getSubscriptionBody(String(subId));
2424
const response = await fetch(`${process.env.CHECKOUT_URL}`, {

pages/api/subscription/index.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
2-
import { getSession, getSubscriptionInfo, setWelcome } from '../../../lib/auth';
2+
import { getSession, getSubscriptionInfo } from '../../../lib/auth';
33

44
export default async function subscription(req: NextApiRequest, res: NextApiResponse) {
55
try {
66
const { plan, storeHash } = await getSession(req);
77
const data = plan ?? await getSubscriptionInfo(storeHash);
88

9-
if (data?.showPaidWelcome) {
10-
// TODO: setWelcome after alert has shown
11-
setTimeout(() => {
12-
setWelcome(storeHash, false);
13-
}, 1000);
14-
}
15-
169
res.status(200).json(data);
1710
} catch (error) {
1811
const { message, response } = error;

pages/index.tsx

+11-6
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ import { useCallback, useEffect } from 'react';
33
import styled from 'styled-components';
44
import ErrorMessage from '../components/error';
55
import Loading from '../components/loading';
6+
import { useSession } from '../context/session';
67
import { plans } from '../lib/checkout';
78
import { useAlerts, useProducts, useSubscription } from '../lib/hooks';
89

910
const Index = () => {
1011
const alertsManager = useAlerts();
1112
const { error, isLoading, summary } = useProducts();
13+
const encodedContext = useSession()?.context;
1214
const { subscription } = useSubscription();
13-
const { pid: appPID, showPaidWelcome } = subscription ?? {};
15+
const { pid: planId, showPaidWelcome } = subscription ?? {};
1416

15-
const getUpgradeAlert = useCallback(() => {
16-
const planName = plans.find(plan => plan.pid === appPID)?.name;
17+
const handleUpgradeMsg = useCallback(async () => {
18+
const planName = plans.find(plan => plan.pid === planId)?.name;
1719
if (!planName) return;
1820

1921
alertsManager.add({
@@ -30,11 +32,14 @@ const Index = () => {
3032
type: 'success',
3133
autoDismiss: true,
3234
});
33-
}, [alertsManager, appPID]);
35+
36+
// Remove alert once shown
37+
await fetch(`/api/checkout/removeWelcome?context=${encodedContext}`);
38+
}, [alertsManager, encodedContext, planId]);
3439

3540
useEffect(() => {
36-
if (showPaidWelcome) getUpgradeAlert();
37-
}, [showPaidWelcome, getUpgradeAlert]);
41+
if (showPaidWelcome) handleUpgradeMsg();
42+
}, [showPaidWelcome, handleUpgradeMsg]);
3843

3944
if (isLoading) return <Loading />;
4045
if (error) return <ErrorMessage error={error} />;

types/db.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface Db {
2323
deleteUser(session: SessionProps): Promise<void>;
2424
setStorePlan?(session: SessionProps): Promise<void>;
2525
setStoreWelcome?(storeHash: string, show: boolean): Promise<void>;
26-
setSubscriptionId?(pid: string, subscriptionId: string): Promise<void>;
26+
setCheckoutId?(pid: string, subscriptionId: string): Promise<void>;
2727
getStorePlan?(storeHash: string): Promise<DocumentData>;
28-
getSubscriptionId?(pid: string): Promise<void> | null;
28+
getCheckoutId?(pid: string): Promise<void> | null;
2929
}

0 commit comments

Comments
 (0)