Skip to content

Commit b5073a0

Browse files
feat: Refactored the hook - useSubOSIntegration and added TypeScript interfaces for SubOS resources
1 parent f19742a commit b5073a0

File tree

1 file changed

+126
-91
lines changed

1 file changed

+126
-91
lines changed

apps/web/hooks/useSubOSIntegration.tsx

Lines changed: 126 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,134 +4,193 @@ import { useAppState } from 'store/app.context';
44
import { CancellationModeEnum } from '@config';
55
import { BILLABLEMETRIC_CODE_ENUM } from '@impler/shared';
66

7+
interface SubOSComponents {
8+
PlanSelector: any;
9+
PlanCard: any;
10+
}
11+
12+
interface SubOSHooks {
13+
useCustomerPortal: any;
14+
}
15+
16+
interface SubOSApis {
17+
plansApi: any;
18+
subscriptionApi: any;
19+
transactionApi?: any;
20+
}
21+
22+
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
23+
interface ITransactionParams {
24+
page?: number;
25+
limit?: number;
26+
}
27+
28+
interface CancelSubscriptionParams {
29+
reasons: string[];
30+
}
31+
32+
const MAX_RETRY_ATTEMPTS = 2;
33+
const DEFAULT_RETRY_DELAY = 1000;
34+
const DEFAULT_TRANSACTION_LIMIT = 10;
35+
const DEFAULT_APP_NAME = 'Impler';
36+
const DEFAULT_APP_VERSION = '1.0.0';
37+
738
export const useSubOSIntegration = () => {
839
const { profileInfo } = useAppState();
940

10-
// SubOS components and utilities
11-
const [subOSComponents, setSubOSComponents] = useState<{
12-
PlanSelector: any;
13-
PlanCard: any;
14-
} | null>(null);
15-
const [subOSHooks, setSubOSHooks] = useState<{
16-
useCustomerPortal: any;
17-
} | null>(null);
18-
const [subOSApis, setSubOSApis] = useState<{
19-
plansApi: any;
20-
subscriptionApi: any;
21-
transactionApi?: any;
22-
} | null>(null);
23-
24-
// Configuration state
41+
const [subOSComponents, setSubOSComponents] = useState<SubOSComponents | null>(null);
42+
const [subOSHooks, setSubOSHooks] = useState<SubOSHooks | null>(null);
43+
const [subOSApis, setSubOSApis] = useState<SubOSApis | null>(null);
44+
2545
const [isConfigured, setIsConfigured] = useState(false);
2646
const [loading, setLoading] = useState(true);
2747
const [error, setError] = useState<string | null>(null);
2848

29-
// Data states
3049
const [subscription, setSubscription] = useState<any>(null);
3150
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
3251

33-
// Track if initial data has been fetched
3452
const initialFetchDone = useRef(false);
3553
const initializingSubOS = useRef(false);
3654

37-
// Initialize SubOS
55+
const handleError = useCallback((err: unknown, context: string) => {
56+
const errorMessage = err instanceof Error ? err.message : String(err);
57+
console.error(`${context}:`, err);
58+
setError(errorMessage);
59+
60+
return errorMessage;
61+
}, []);
62+
63+
const isNetworkError = (err: unknown): boolean => {
64+
return err instanceof Error && err.message.includes('network');
65+
};
66+
67+
const delay = (ms: number): Promise<void> => {
68+
return new Promise((resolve) => setTimeout(resolve, ms));
69+
};
70+
71+
const clearError = useCallback(() => {
72+
setError(null);
73+
}, []);
74+
3875
const initializeSubOS = useCallback(async () => {
39-
if (initializingSubOS.current || isConfigured) return;
76+
if (initializingSubOS.current || isConfigured) {
77+
return;
78+
}
4079

4180
initializingSubOS.current = true;
81+
4282
try {
4383
setLoading(true);
4484
setError(null);
4585

46-
// Configure SubOS using environment variables with fallbacks
86+
// Configure/Initilize SubOS
4787
configureSubOS({
4888
apiEndpoint: process.env.NEXT_PUBLIC_SUBOS_API_ENDPOINT,
4989
projectId: process.env.NEXT_PUBLIC_SUBOS_PROJECT_ID,
5090
stripePublishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
51-
appName: process.env.NEXT_PUBLIC_APP_NAME || 'Impler',
91+
appName: process.env.NEXT_PUBLIC_APP_NAME || DEFAULT_APP_NAME,
5292
appEnvironment: process.env.NODE_ENV,
53-
appVersion: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
93+
appVersion: process.env.NEXT_PUBLIC_APP_VERSION || DEFAULT_APP_VERSION,
5494
debug: process.env.NODE_ENV === 'development',
5595
});
5696

57-
// Import SubOS module
5897
const subos = await import('subos-frontend');
5998

60-
// Extract components
61-
const components = {
99+
setSubOSComponents({
62100
PlanSelector: subos.PlanSelector,
63101
PlanCard: subos.PlanCard,
64-
};
102+
});
65103

66-
// Extract hooks
67-
const hooks = {
104+
setSubOSHooks({
68105
useCustomerPortal: subos.useCustomerPortal,
69-
};
106+
});
70107

71-
// Extract APIs
72-
const apis = {
108+
setSubOSApis({
73109
plansApi: subos.plansApi,
74110
subscriptionApi: subos.subscriptionApi,
75111
transactionApi: subos.transactionApi,
76-
};
112+
});
77113

78-
setSubOSComponents(components);
79-
setSubOSHooks(hooks);
80-
setSubOSApis(apis);
81114
setIsConfigured(true);
82115
} catch (err) {
83-
console.error('Failed to initialize SubOS:', err);
84-
setError(err instanceof Error ? err.message : String(err));
116+
handleError(err, 'Failed to initialize SubOS');
85117
} finally {
86118
setLoading(false);
87119
initializingSubOS.current = false;
88120
}
89-
}, [isConfigured]);
121+
}, [isConfigured, handleError]);
90122

91-
// Fetch subscription with retry logic
92123
const fetchSubscription = useCallback(
93124
async (retryCount = 0): Promise<any> => {
94125
if (!subOSApis?.subscriptionApi || !isConfigured || !profileInfo?.email) {
95126
return null;
96127
}
97128

98129
try {
99-
const subscriptionData = await subOSApis.subscriptionApi.getActiveSubscription(profileInfo.email);
130+
const response = await subOSApis.subscriptionApi.getActiveSubscription(profileInfo.email);
100131

101-
if (subscriptionData?.data) {
102-
setSubscription(subscriptionData.data);
132+
if (response?.data) {
133+
setSubscription(response.data);
103134
}
104135

105-
return subscriptionData;
136+
return response;
106137
} catch (err) {
107-
console.error('Error fetching subscription:', err);
108-
if (retryCount < 2 && err instanceof Error && err.message.includes('network')) {
109-
await new Promise((resolve) => setTimeout(resolve, 1000 * (retryCount + 1)));
138+
// Retry logic for network errors
139+
if (retryCount < MAX_RETRY_ATTEMPTS && isNetworkError(err)) {
140+
await delay(DEFAULT_RETRY_DELAY * (retryCount + 1));
110141

111142
return fetchSubscription(retryCount + 1);
112143
}
113-
setError(err instanceof Error ? err.message : String(err));
144+
145+
handleError(err, 'Error fetching subscription');
114146

115147
return null;
116148
}
117149
},
118-
[subOSApis, isConfigured, profileInfo?.email]
150+
[subOSApis, isConfigured, profileInfo?.email, handleError]
151+
);
152+
153+
const cancelSubscription = useCallback(
154+
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
155+
async ({ reasons }: CancelSubscriptionParams) => {
156+
// Validation
157+
if (!subOSApis?.subscriptionApi || !isConfigured || !profileInfo?.email) {
158+
throw new Error('SubOS API not properly configured or email not available');
159+
}
160+
161+
try {
162+
const response = await subOSApis.subscriptionApi.cancelSubscription(profileInfo.email, {
163+
cancellationMode: CancellationModeEnum.END_OF_PERIOD,
164+
});
165+
166+
return response.data;
167+
} catch (err) {
168+
handleError(err, 'Error cancelling subscription');
169+
throw err;
170+
}
171+
},
172+
[subOSApis, isConfigured, profileInfo?.email, handleError]
119173
);
120174

121-
// Handle plan selection and checkout
122175
const selectPlan = useCallback(
123176
async (plan: Plan) => {
124-
if (!subOSApis?.plansApi || !profileInfo?.email) return null;
177+
// Validation
178+
if (!subOSApis?.plansApi || !profileInfo?.email) {
179+
return null;
180+
}
125181

126182
try {
127183
setSelectedPlan(plan);
184+
128185
const response = await subOSApis.plansApi.createPaymentSession(plan.code, {
129186
returnUrl: `${window.location.origin}/subscription_status`,
130187
externalId: profileInfo.email,
131188
billableMetricCode: BILLABLEMETRIC_CODE_ENUM.ROWS,
132189
});
133190

191+
// Extract checkout URL
134192
const checkoutUrl = response?.data?.checkoutUrl || response?.data?.url;
193+
135194
if (response?.success && checkoutUrl) {
136195
window.location.href = checkoutUrl;
137196

@@ -140,18 +199,17 @@ export const useSubOSIntegration = () => {
140199

141200
return null;
142201
} catch (err) {
143-
console.error('Error selecting plan:', err);
144-
setError(err instanceof Error ? err.message : String(err));
202+
handleError(err, 'Error selecting plan');
145203

146204
return null;
147205
}
148206
},
149-
[subOSApis, profileInfo?.email]
207+
[subOSApis, profileInfo?.email, handleError]
150208
);
151209

152-
// Fetch transactions
153210
const fetchTransactions = useCallback(
154-
async (page = 1, limit = 10) => {
211+
async (page = 1, limit = DEFAULT_TRANSACTION_LIMIT) => {
212+
// Validation
155213
if (!subOSApis?.transactionApi || !isConfigured || !profileInfo?.email) {
156214
return [];
157215
}
@@ -161,44 +219,18 @@ export const useSubOSIntegration = () => {
161219

162220
return response?.data || [];
163221
} catch (err) {
164-
console.error('Error fetching transactions:', err);
165-
setError(err instanceof Error ? err.message : String(err));
222+
handleError(err, 'Error fetching transactions');
166223

167224
return [];
168225
}
169226
},
170-
[subOSApis, isConfigured, profileInfo?.email]
171-
);
172-
173-
// Handle subscription cancellation
174-
const cancelSubscription = useCallback(
175-
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
176-
async ({ reasons }: { reasons: string[] }) => {
177-
if (!subOSApis?.subscriptionApi || !isConfigured || !profileInfo?.email) {
178-
throw new Error('SubOS API not properly configured or email not available');
179-
}
180-
181-
try {
182-
const response = await subOSApis.subscriptionApi.cancelSubscription(profileInfo.email, {
183-
cancellationMode: CancellationModeEnum.END_OF_PERIOD,
184-
});
185-
186-
return response.data;
187-
} catch (err) {
188-
console.error('Error cancelling subscription:', err);
189-
setError(err instanceof Error ? err.message : String(err));
190-
throw err;
191-
}
192-
},
193-
[subOSApis, isConfigured, profileInfo?.email]
227+
[subOSApis, isConfigured, profileInfo?.email, handleError]
194228
);
195229

196-
// Initialize on mount
197230
useEffect(() => {
198231
initializeSubOS();
199232
}, [initializeSubOS]);
200233

201-
// Fetch initial data when configured
202234
useEffect(() => {
203235
if (isConfigured && !initialFetchDone.current) {
204236
initialFetchDone.current = true;
@@ -207,28 +239,31 @@ export const useSubOSIntegration = () => {
207239
}, [isConfigured, fetchSubscription]);
208240

209241
return {
210-
// Configuration state
211242
isConfigured,
212243
loading,
213244
error,
214245

215-
// SubOS resources
246+
// SubOS Resources
216247
components: subOSComponents,
217248
hooks: subOSHooks,
218249
apis: subOSApis,
219250

220-
// Data
251+
// Data State
221252
subscription,
222253
selectedPlan,
223254

224-
// Actions
255+
// Subscription Actions
225256
fetchSubscription,
226-
fetchTransactions,
227-
selectPlan,
228257
cancelSubscription,
229258

230-
// Utilities
259+
// Plan Actions
260+
selectPlan,
261+
262+
// Transaction Actions
263+
fetchTransactions,
264+
265+
// Utility Actions
231266
reinitialize: initializeSubOS,
232-
clearError: () => setError(null),
267+
clearError,
233268
};
234269
};

0 commit comments

Comments
 (0)