@edx/frontend-app-alacarte-checkout 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +2 -0
  3. package/README.rst +235 -0
  4. package/dist/Main.d.ts +3 -0
  5. package/dist/Main.js +15 -0
  6. package/dist/Main.js.map +1 -0
  7. package/dist/app.d.ts +3 -0
  8. package/dist/app.js +12 -0
  9. package/dist/app.js.map +1 -0
  10. package/dist/checkoutPage/CheckoutPage.d.ts +3 -0
  11. package/dist/checkoutPage/CheckoutPage.js +26 -0
  12. package/dist/checkoutPage/CheckoutPage.js.map +1 -0
  13. package/dist/components/BrandMarks/BrandMarks.d.ts +7 -0
  14. package/dist/components/BrandMarks/BrandMarks.js +9 -0
  15. package/dist/components/BrandMarks/BrandMarks.js.map +1 -0
  16. package/dist/components/CartSummary/CartSummary.d.ts +26 -0
  17. package/dist/components/CartSummary/CartSummary.js +41 -0
  18. package/dist/components/CartSummary/CartSummary.js.map +1 -0
  19. package/dist/components/CartSummary/Messages.d.ts +103 -0
  20. package/dist/components/CartSummary/Messages.js +106 -0
  21. package/dist/components/CartSummary/Messages.js.map +1 -0
  22. package/dist/components/Checkout/Checkout.d.ts +9 -0
  23. package/dist/components/Checkout/Checkout.js +82 -0
  24. package/dist/components/Checkout/Checkout.js.map +1 -0
  25. package/dist/components/Checkout/Messages.d.ts +8 -0
  26. package/dist/components/Checkout/Messages.js +10 -0
  27. package/dist/components/Checkout/Messages.js.map +1 -0
  28. package/dist/components/CheckoutGate/CheckoutGate.d.ts +9 -0
  29. package/dist/components/CheckoutGate/CheckoutGate.js +40 -0
  30. package/dist/components/CheckoutGate/CheckoutGate.js.map +1 -0
  31. package/dist/components/CheckoutGate/Messages.d.ts +8 -0
  32. package/dist/components/CheckoutGate/Messages.js +10 -0
  33. package/dist/components/CheckoutGate/Messages.js.map +1 -0
  34. package/dist/components/CountryUnavailableModal/CountryUnavailableModal.d.ts +9 -0
  35. package/dist/components/CountryUnavailableModal/CountryUnavailableModal.js +10 -0
  36. package/dist/components/CountryUnavailableModal/CountryUnavailableModal.js.map +1 -0
  37. package/dist/components/CountryUnavailableModal/Messages.d.ts +18 -0
  38. package/dist/components/CountryUnavailableModal/Messages.js +20 -0
  39. package/dist/components/CountryUnavailableModal/Messages.js.map +1 -0
  40. package/dist/components/CourseEnrollmentCard/CourseEnrollmentCard.d.ts +9 -0
  41. package/dist/components/CourseEnrollmentCard/CourseEnrollmentCard.js +31 -0
  42. package/dist/components/CourseEnrollmentCard/CourseEnrollmentCard.js.map +1 -0
  43. package/dist/components/CourseEnrollmentCard/Messages.d.ts +33 -0
  44. package/dist/components/CourseEnrollmentCard/Messages.js +35 -0
  45. package/dist/components/CourseEnrollmentCard/Messages.js.map +1 -0
  46. package/dist/components/EdxUnlimitedBanner/EdxUnlimitedBanner.d.ts +13 -0
  47. package/dist/components/EdxUnlimitedBanner/EdxUnlimitedBanner.js +11 -0
  48. package/dist/components/EdxUnlimitedBanner/EdxUnlimitedBanner.js.map +1 -0
  49. package/dist/components/EnrollmentSummary/EnrollmentSummary.d.ts +22 -0
  50. package/dist/components/EnrollmentSummary/EnrollmentSummary.js +33 -0
  51. package/dist/components/EnrollmentSummary/EnrollmentSummary.js.map +1 -0
  52. package/dist/components/EnrollmentSummary/Messages.d.ts +48 -0
  53. package/dist/components/EnrollmentSummary/Messages.js +50 -0
  54. package/dist/components/EnrollmentSummary/Messages.js.map +1 -0
  55. package/dist/components/PaymentFields/PaymentFields.d.ts +9 -0
  56. package/dist/components/PaymentFields/PaymentFields.js +7 -0
  57. package/dist/components/PaymentFields/PaymentFields.js.map +1 -0
  58. package/dist/components/PaymentForm/Messages.d.ts +118 -0
  59. package/dist/components/PaymentForm/Messages.js +125 -0
  60. package/dist/components/PaymentForm/Messages.js.map +1 -0
  61. package/dist/components/PaymentForm/PaymentForm.d.ts +28 -0
  62. package/dist/components/PaymentForm/PaymentForm.js +121 -0
  63. package/dist/components/PaymentForm/PaymentForm.js.map +1 -0
  64. package/dist/components/StatusScreens/Messages.d.ts +108 -0
  65. package/dist/components/StatusScreens/Messages.js +114 -0
  66. package/dist/components/StatusScreens/Messages.js.map +1 -0
  67. package/dist/components/StatusScreens/StatusScreens.d.ts +24 -0
  68. package/dist/components/StatusScreens/StatusScreens.js +32 -0
  69. package/dist/components/StatusScreens/StatusScreens.js.map +1 -0
  70. package/dist/constants.d.ts +59 -0
  71. package/dist/constants.js +282 -0
  72. package/dist/constants.js.map +1 -0
  73. package/dist/data/hooks/keys.d.ts +14 -0
  74. package/dist/data/hooks/keys.js +15 -0
  75. package/dist/data/hooks/keys.js.map +1 -0
  76. package/dist/data/hooks/useCheckout.d.ts +61 -0
  77. package/dist/data/hooks/useCheckout.js +343 -0
  78. package/dist/data/hooks/useCheckout.js.map +1 -0
  79. package/dist/data/hooks/useCheckoutGate.d.ts +16 -0
  80. package/dist/data/hooks/useCheckoutGate.js +61 -0
  81. package/dist/data/hooks/useCheckoutGate.js.map +1 -0
  82. package/dist/data/hooks/useCheckoutMutations.d.ts +22 -0
  83. package/dist/data/hooks/useCheckoutMutations.js +166 -0
  84. package/dist/data/hooks/useCheckoutMutations.js.map +1 -0
  85. package/dist/data/hooks/useCheckoutQueries.d.ts +32 -0
  86. package/dist/data/hooks/useCheckoutQueries.js +107 -0
  87. package/dist/data/hooks/useCheckoutQueries.js.map +1 -0
  88. package/dist/data/services/subs/urls.d.ts +2 -0
  89. package/dist/data/services/subs/urls.js +4 -0
  90. package/dist/data/services/subs/urls.js.map +1 -0
  91. package/dist/i18n/index.d.ts +1 -0
  92. package/dist/i18n/index.js +2 -0
  93. package/dist/i18n/index.js.map +1 -0
  94. package/dist/index.d.ts +2 -0
  95. package/dist/index.js +3 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/messages.d.ts +8 -0
  98. package/dist/messages.js +10 -0
  99. package/dist/messages.js.map +1 -0
  100. package/dist/routes.d.ts +18 -0
  101. package/dist/routes.js +47 -0
  102. package/dist/routes.js.map +1 -0
  103. package/dist/slots/ExampleSlot/ExampleSlot.d.ts +3 -0
  104. package/dist/slots/ExampleSlot/ExampleSlot.js +6 -0
  105. package/dist/slots/ExampleSlot/ExampleSlot.js.map +1 -0
  106. package/dist/slots.d.ts +3 -0
  107. package/dist/slots.js +3 -0
  108. package/dist/slots.js.map +1 -0
  109. package/dist/style.scss +14 -0
  110. package/dist/styles/components/brandMarks.scss +41 -0
  111. package/dist/styles/components/cartSummary.scss +29 -0
  112. package/dist/styles/components/checkout.scss +29 -0
  113. package/dist/styles/components/checkoutStepper.scss +9 -0
  114. package/dist/styles/components/courseEnrollmentCard.scss +38 -0
  115. package/dist/styles/components/edxUnlimitedBanner.scss +138 -0
  116. package/dist/styles/components/enrollmentSummary.scss +19 -0
  117. package/dist/styles/components/paymentFields.scss +15 -0
  118. package/dist/styles/components/paymentForm.scss +139 -0
  119. package/dist/styles/components/statusScreens.scss +39 -0
  120. package/dist/styles/components/successScreen.scss +0 -0
  121. package/dist/types.d.ts +232 -0
  122. package/dist/types.js +2 -0
  123. package/dist/types.js.map +1 -0
  124. package/dist/utils/auth.d.ts +7 -0
  125. package/dist/utils/auth.js +28 -0
  126. package/dist/utils/auth.js.map +1 -0
  127. package/dist/utils/billingDetails.d.ts +30 -0
  128. package/dist/utils/billingDetails.js +48 -0
  129. package/dist/utils/billingDetails.js.map +1 -0
  130. package/dist/utils/bootstrapApi.d.ts +20 -0
  131. package/dist/utils/bootstrapApi.js +142 -0
  132. package/dist/utils/bootstrapApi.js.map +1 -0
  133. package/dist/utils/cartApi.d.ts +16 -0
  134. package/dist/utils/cartApi.js +133 -0
  135. package/dist/utils/cartApi.js.map +1 -0
  136. package/dist/utils/checkout.d.ts +6 -0
  137. package/dist/utils/checkout.js +28 -0
  138. package/dist/utils/checkout.js.map +1 -0
  139. package/dist/utils/checkoutErrors.d.ts +1 -0
  140. package/dist/utils/checkoutErrors.js +11 -0
  141. package/dist/utils/checkoutErrors.js.map +1 -0
  142. package/dist/utils/fetchActiveSubscriptions.d.ts +4 -0
  143. package/dist/utils/fetchActiveSubscriptions.js +36 -0
  144. package/dist/utils/fetchActiveSubscriptions.js.map +1 -0
  145. package/dist/utils/fetchBillingPlans.d.ts +2 -0
  146. package/dist/utils/fetchBillingPlans.js +48 -0
  147. package/dist/utils/fetchBillingPlans.js.map +1 -0
  148. package/dist/utils/graphqlClient.d.ts +16 -0
  149. package/dist/utils/graphqlClient.js +39 -0
  150. package/dist/utils/graphqlClient.js.map +1 -0
  151. package/dist/utils/index.d.ts +3 -0
  152. package/dist/utils/index.js +17 -0
  153. package/dist/utils/index.js.map +1 -0
  154. package/dist/utils/planQuery.d.ts +16 -0
  155. package/dist/utils/planQuery.js +57 -0
  156. package/dist/utils/planQuery.js.map +1 -0
  157. package/dist/utils/provision.d.ts +48 -0
  158. package/dist/utils/provision.js +161 -0
  159. package/dist/utils/provision.js.map +1 -0
  160. package/dist/utils/queries.d.ts +35 -0
  161. package/dist/utils/queries.js +168 -0
  162. package/dist/utils/queries.js.map +1 -0
  163. package/dist/utils/stripe.d.ts +23 -0
  164. package/dist/utils/stripe.js +86 -0
  165. package/dist/utils/stripe.js.map +1 -0
  166. package/package.json +85 -0
@@ -0,0 +1,343 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
11
+ import { createCheckoutClient } from '../../utils/checkout';
12
+ import { getPlanFromUrl, writePlanQueryParam } from '../../utils/planQuery';
13
+ import { getReturnedCartBiIdFromUrl, loadStripe } from '../../utils/stripe';
14
+ import { STATUS, STATUS_LABELS, STRIPE_ELEMENT_OPTS, phaseFor, DEFAULTS, BILLING_COUNTRIES, } from '../../constants';
15
+ import { useCartStatusQuery, useSubscriptionIdQuery, useBillingPlansQuery, useActiveSubscriptionsQuery, useViaxSessionQuery, useCourseDetailQuery, } from './useCheckoutQueries';
16
+ import { useCheckoutMutations } from './useCheckoutMutations';
17
+ /**
18
+ * Read ?course_run_key= from the current URL.
19
+ * Avoid URLSearchParams — it treats bare `+` as a space, which would turn
20
+ * HarvardX+CS50+X into "HarvardX CS50 X". decodeURIComponent keeps `+` intact.
21
+ */
22
+ function getCourseRunKeyFromUrl() {
23
+ if (typeof window === 'undefined')
24
+ return null;
25
+ const match = /[?&]course_run_key=([^&]*)/.exec(window.location.search);
26
+ if (!match)
27
+ return null;
28
+ try {
29
+ // Preserve literal `+` (treat as %2B) then decode %XX sequences.
30
+ return decodeURIComponent(match[1].replace(/\+/g, '%2B')).trim() || null;
31
+ }
32
+ catch (_a) {
33
+ return match[1].trim() || null;
34
+ }
35
+ }
36
+ const redirectBrowser = (url) => {
37
+ window.location.assign(url);
38
+ };
39
+ const getInitialBillingCountry = (userCountry) => {
40
+ const normalized = userCountry === null || userCountry === void 0 ? void 0 : userCountry.trim().toUpperCase();
41
+ if (normalized && BILLING_COUNTRIES.some(([countryCode]) => countryCode === normalized)) {
42
+ return normalized;
43
+ }
44
+ return 'US';
45
+ };
46
+ export default function useCheckout(options = {}) {
47
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
48
+ const { apiUrl = DEFAULTS.apiUrl, edxSessionUrl, stripePublishableKey, onComplete, userCountry = null, courseRunKey: courseRunKeyProp, redirectToUrl = redirectBrowser, } = options;
49
+ const courseRunKey = useMemo(() => courseRunKeyProp !== null && courseRunKeyProp !== void 0 ? courseRunKeyProp : getCourseRunKeyFromUrl(), [courseRunKeyProp]);
50
+ const client = useMemo(() => {
51
+ if (!apiUrl)
52
+ return null;
53
+ return createCheckoutClient({ apiUrl });
54
+ }, [apiUrl]);
55
+ const notConfigured = !client || !stripePublishableKey || !edxSessionUrl;
56
+ // Exchange edX session for viax token on page load.
57
+ const sessionQuery = useViaxSessionQuery(notConfigured ? null : edxSessionUrl);
58
+ const sessionReady = !!sessionQuery.data;
59
+ useEffect(() => {
60
+ if (sessionQuery.data && client) {
61
+ client.setToken(sessionQuery.data.viaxToken);
62
+ }
63
+ }, [sessionQuery.data, client]);
64
+ // Fetch billing plans, subscriber count, and optional course once the viax session is ready.
65
+ const billingPlansQuery = useBillingPlansQuery(client, sessionReady);
66
+ const activeSubscriptionsQuery = useActiveSubscriptionsQuery(client, sessionReady);
67
+ const courseDetailQuery = useCourseDetailQuery(client, sessionReady, courseRunKey);
68
+ // --- state ---
69
+ const [showBanner, setShowBanner] = useState(true);
70
+ const [cart, setCart] = useState(null);
71
+ const [stripe, setStripe] = useState(null);
72
+ const [stripeReady, setStripeReady] = useState(false);
73
+ const [cardReady, setCardReady] = useState(false);
74
+ const [error, setError] = useState('');
75
+ const [cardError, setCardError] = useState('');
76
+ const [billing, setBilling] = useState({
77
+ firstName: '', lastName: '', city: '', zip: '', country: getInitialBillingCountry(userCountry), line1: '', state: '',
78
+ });
79
+ const [selectedCycle, setSelectedCycle] = useState(getPlanFromUrl);
80
+ // Set when Stripe returns from a 3DS challenge; cleared once a new attempt starts.
81
+ const [returnedCartBiId, setReturnedCartBiId] = useState(getReturnedCartBiIdFromUrl);
82
+ const [processingSubscription, setProcessingSubscription] = useState(() => getReturnedCartBiIdFromUrl() !== null);
83
+ const setCycle = useCallback((cycle) => {
84
+ setSelectedCycle(cycle);
85
+ writePlanQueryParam(cycle);
86
+ }, []);
87
+ useEffect(() => {
88
+ if (sessionQuery.error) {
89
+ setError(sessionQuery.error.message);
90
+ }
91
+ }, [sessionQuery.error]);
92
+ const setField = useCallback((field, value) => setBilling((p) => (Object.assign(Object.assign({}, p), { [field]: value }))), []);
93
+ // --- refs ---
94
+ // Stripe instances — written inside useEffect and mutations.
95
+ const stripeInstanceRef = useRef(null);
96
+ const cardNumberElementRef = useRef(null);
97
+ const cardExpiryElementRef = useRef(null);
98
+ const cardCvcElementRef = useRef(null);
99
+ const elementsRef = useRef(null);
100
+ // DOM mount targets — callback refs so we remount when hosts leave and return.
101
+ const numberMountNodeRef = useRef(null);
102
+ const expiryMountNodeRef = useRef(null);
103
+ const cvcMountNodeRef = useRef(null);
104
+ const [cardHostsEpoch, setCardHostsEpoch] = useState(0);
105
+ const assignCardHost = useCallback((store, node) => {
106
+ var _a, _b, _c, _d, _e, _f;
107
+ store.current = node;
108
+ if (numberMountNodeRef.current
109
+ && expiryMountNodeRef.current
110
+ && cvcMountNodeRef.current) {
111
+ setCardHostsEpoch((n) => n + 1);
112
+ return;
113
+ }
114
+ // Hosts are leaving the DOM — detach so Stripe is not bound to dead nodes.
115
+ (_b = (_a = cardNumberElementRef.current) === null || _a === void 0 ? void 0 : _a.unmount) === null || _b === void 0 ? void 0 : _b.call(_a);
116
+ (_d = (_c = cardExpiryElementRef.current) === null || _c === void 0 ? void 0 : _c.unmount) === null || _d === void 0 ? void 0 : _d.call(_c);
117
+ (_f = (_e = cardCvcElementRef.current) === null || _e === void 0 ? void 0 : _e.unmount) === null || _f === void 0 ? void 0 : _f.call(_e);
118
+ }, []);
119
+ const numberMountRef = useCallback((node) => {
120
+ assignCardHost(numberMountNodeRef, node);
121
+ }, [assignCardHost]);
122
+ const expiryMountRef = useCallback((node) => {
123
+ assignCardHost(expiryMountNodeRef, node);
124
+ }, [assignCardHost]);
125
+ const cvcMountRef = useCallback((node) => {
126
+ assignCardHost(cvcMountNodeRef, node);
127
+ }, [assignCardHost]);
128
+ const mountedRef = useRef(true);
129
+ const completedRef = useRef(false);
130
+ const redirectedUrlRef = useRef(null);
131
+ const sessionRef = useRef(null);
132
+ useEffect(() => {
133
+ var _a;
134
+ sessionRef.current = (_a = sessionQuery.data) !== null && _a !== void 0 ? _a : null;
135
+ }, [sessionQuery.data]);
136
+ // Mirror state into refs so mutations can read current values without stale closures.
137
+ const cartRef = useRef(null);
138
+ const billingRef = useRef(billing);
139
+ const selectedCycleRef = useRef(selectedCycle);
140
+ useEffect(() => {
141
+ cartRef.current = cart;
142
+ }, [cart]);
143
+ useEffect(() => {
144
+ billingRef.current = billing;
145
+ }, [billing]);
146
+ useEffect(() => {
147
+ selectedCycleRef.current = selectedCycle;
148
+ }, [selectedCycle]);
149
+ useEffect(() => {
150
+ stripeInstanceRef.current = stripe;
151
+ }, [stripe]);
152
+ // --- server state ---
153
+ // Poll the cart created in this session, or the one Stripe just returned
154
+ // from. The returned cart waits for the viax session so the status request
155
+ // carries a valid token.
156
+ const cartBiId = (_a = cart === null || cart === void 0 ? void 0 : cart.biId) !== null && _a !== void 0 ? _a : (sessionReady ? returnedCartBiId : null);
157
+ const statusQuery = useCartStatusQuery(client, cartBiId);
158
+ const status = (_c = (_b = statusQuery.data) === null || _b === void 0 ? void 0 : _b.status) !== null && _c !== void 0 ? _c : null;
159
+ const lastComment = (_e = (_d = statusQuery.data) === null || _d === void 0 ? void 0 : _d.lastComment) !== null && _e !== void 0 ? _e : '';
160
+ // Phase is 'ready' until a cart exists, then driven by bpStatus.
161
+ const phase = cartBiId ? phaseFor(status, status === STATUS.IN_CART) : 'ready';
162
+ const subscriptionIdQuery = useSubscriptionIdQuery(client, cartBiId, status === STATUS.SUBSCRIPTION_CREATED);
163
+ // --- mutations ---
164
+ const { placeOrder: placeMutation } = useCheckoutMutations({
165
+ client,
166
+ sessionRef,
167
+ stripeRef: stripeInstanceRef,
168
+ cardNumberElRef: cardNumberElementRef,
169
+ billingRef,
170
+ selectedCycleRef,
171
+ setError: (msg) => {
172
+ setProcessingSubscription(false);
173
+ setError(msg);
174
+ },
175
+ setCardError: (msg) => {
176
+ setProcessingSubscription(false);
177
+ setCardError(msg);
178
+ },
179
+ onCartCreated: (created) => {
180
+ // Sync the ref immediately so any same-tick readers see the cart
181
+ // before the setCart → useEffect mirror runs.
182
+ cartRef.current = created;
183
+ if (mountedRef.current)
184
+ setCart(created);
185
+ },
186
+ });
187
+ // --- load Stripe.js on mount ---
188
+ useEffect(() => {
189
+ mountedRef.current = true;
190
+ if (!stripePublishableKey)
191
+ return undefined;
192
+ (() => __awaiter(this, void 0, void 0, function* () {
193
+ try {
194
+ const instance = yield loadStripe(stripePublishableKey);
195
+ if (mountedRef.current) {
196
+ setStripe(instance);
197
+ setStripeReady(true);
198
+ }
199
+ }
200
+ catch (e) {
201
+ if (mountedRef.current)
202
+ setError(e.message);
203
+ }
204
+ }))();
205
+ return () => {
206
+ mountedRef.current = false;
207
+ };
208
+ }, [stripePublishableKey]);
209
+ // --- mount Stripe Elements once Stripe is loaded; remount if hosts return ---
210
+ useEffect(() => {
211
+ var _a, _b, _c, _d, _e, _f;
212
+ if (!stripe)
213
+ return;
214
+ const numberNode = numberMountNodeRef.current;
215
+ const expiryNode = expiryMountNodeRef.current;
216
+ const cvcNode = cvcMountNodeRef.current;
217
+ if (!numberNode || !expiryNode || !cvcNode)
218
+ return;
219
+ if (cardNumberElementRef.current && cardExpiryElementRef.current && cardCvcElementRef.current) {
220
+ try {
221
+ (_b = (_a = cardNumberElementRef.current).unmount) === null || _b === void 0 ? void 0 : _b.call(_a);
222
+ (_d = (_c = cardExpiryElementRef.current).unmount) === null || _d === void 0 ? void 0 : _d.call(_c);
223
+ (_f = (_e = cardCvcElementRef.current).unmount) === null || _f === void 0 ? void 0 : _f.call(_e);
224
+ cardNumberElementRef.current.mount(numberNode);
225
+ cardExpiryElementRef.current.mount(expiryNode);
226
+ cardCvcElementRef.current.mount(cvcNode);
227
+ return;
228
+ }
229
+ catch (_g) {
230
+ // Element is bound to a destroyed host — create a fresh Elements group.
231
+ cardNumberElementRef.current = null;
232
+ cardExpiryElementRef.current = null;
233
+ cardCvcElementRef.current = null;
234
+ elementsRef.current = null;
235
+ }
236
+ }
237
+ setCardReady(false);
238
+ const elements = stripe.elements();
239
+ elementsRef.current = elements;
240
+ const number = elements.create('cardNumber', Object.assign(Object.assign({}, STRIPE_ELEMENT_OPTS), { showIcon: true }));
241
+ const expiry = elements.create('cardExpiry', STRIPE_ELEMENT_OPTS);
242
+ const cvc = elements.create('cardCvc', STRIPE_ELEMENT_OPTS);
243
+ number.mount(numberNode);
244
+ expiry.mount(expiryNode);
245
+ cvc.mount(cvcNode);
246
+ number.on('ready', () => {
247
+ if (mountedRef.current)
248
+ setCardReady(true);
249
+ });
250
+ number.on('change', (e) => {
251
+ var _a;
252
+ if (mountedRef.current)
253
+ setCardError(((_a = e.error) === null || _a === void 0 ? void 0 : _a.message) || '');
254
+ });
255
+ cardNumberElementRef.current = number;
256
+ cardExpiryElementRef.current = expiry;
257
+ cardCvcElementRef.current = cvc;
258
+ }, [stripe, cardHostsEpoch]);
259
+ // --- on success ---
260
+ useEffect(() => {
261
+ var _a, _b;
262
+ if (status !== STATUS.SUBSCRIPTION_CREATED || completedRef.current)
263
+ return;
264
+ completedRef.current = true;
265
+ if (onComplete)
266
+ onComplete((_b = (_a = subscriptionIdQuery.data) === null || _a === void 0 ? void 0 : _a.biId) !== null && _b !== void 0 ? _b : null);
267
+ }, [status, subscriptionIdQuery.data, onComplete]);
268
+ // Stripe and PayPal both authenticate the buyer on a hosted page before
269
+ // returning the browser to checkout, so this must be a full-page
270
+ // navigation (not an API request) for either flow.
271
+ useEffect(() => {
272
+ var _a, _b;
273
+ const redirectUrl = ((_a = statusQuery.data) === null || _a === void 0 ? void 0 : _a.stripeConfirmationRedirectUrl)
274
+ || ((_b = statusQuery.data) === null || _b === void 0 ? void 0 : _b.paypalApprovalUrl);
275
+ if (status !== STATUS.STRIPE_CONFIRMATION_REQUIRED
276
+ || !redirectUrl
277
+ || redirectedUrlRef.current === redirectUrl)
278
+ return;
279
+ redirectedUrlRef.current = redirectUrl;
280
+ redirectToUrl(redirectUrl);
281
+ }, [
282
+ status,
283
+ (_f = statusQuery.data) === null || _f === void 0 ? void 0 : _f.stripeConfirmationRedirectUrl,
284
+ (_g = statusQuery.data) === null || _g === void 0 ? void 0 : _g.paypalApprovalUrl,
285
+ redirectToUrl,
286
+ ]);
287
+ // --- actions ---
288
+ const placeOrder = useCallback((consent, paymentMethod = 'card') => {
289
+ setError('');
290
+ setCardError('');
291
+ if (phase === 'failed')
292
+ setCart(null);
293
+ // Each attempt creates its own cart, so stop reporting on the returned one.
294
+ setReturnedCartBiId(null);
295
+ setProcessingSubscription(true);
296
+ placeMutation.mutate({ consent, paymentMethod });
297
+ }, [placeMutation, phase]);
298
+ // Turn off processingSubscription once we reach a terminal status.
299
+ useEffect(() => {
300
+ if (!status)
301
+ return;
302
+ const terminalStatuses = [
303
+ STATUS.SUBSCRIPTION_CREATED,
304
+ STATUS.STRIPE_FAIL,
305
+ STATUS.SANCTION_BLOCK,
306
+ ];
307
+ if (terminalStatuses.includes(status)) {
308
+ setProcessingSubscription(false);
309
+ }
310
+ }, [status]);
311
+ return {
312
+ phase,
313
+ status,
314
+ statusLabel: (status && STATUS_LABELS[status]) || 'Processing your order…',
315
+ stripeReady,
316
+ cardReady,
317
+ notConfigured,
318
+ showBanner,
319
+ dismissBanner: () => setShowBanner(false),
320
+ error,
321
+ clearError: () => setError(''),
322
+ cardError,
323
+ lastComment,
324
+ subscriptionDetails: (_h = subscriptionIdQuery.data) !== null && _h !== void 0 ? _h : null,
325
+ billing,
326
+ setField,
327
+ numberMountRef,
328
+ expiryMountRef,
329
+ cvcMountRef,
330
+ submitting: placeMutation.isPending,
331
+ processingSubscription,
332
+ returningFromStripe: returnedCartBiId !== null,
333
+ placeOrder,
334
+ billingPlans: (_k = (_j = billingPlansQuery.data) === null || _j === void 0 ? void 0 : _j.plans) !== null && _k !== void 0 ? _k : [],
335
+ activeSubscriberCount: (_m = (_l = activeSubscriptionsQuery.data) === null || _l === void 0 ? void 0 : _l.active) !== null && _m !== void 0 ? _m : null,
336
+ selectedCycle,
337
+ setCycle,
338
+ sessionReady,
339
+ userCountry,
340
+ course: (_o = courseDetailQuery.data) !== null && _o !== void 0 ? _o : null,
341
+ };
342
+ }
343
+ //# sourceMappingURL=useCheckout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCheckout.js","sourceRoot":"","sources":["../../../src/data/hooks/useCheckout.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n useCallback, useEffect, useMemo, useRef, useState,\n} from 'react';\nimport type { Ref } from 'react';\nimport type { BillingCycle } from '../../types';\nimport { createCheckoutClient } from '../../utils/checkout';\nimport { getPlanFromUrl, writePlanQueryParam } from '../../utils/planQuery';\nimport { getReturnedCartBiIdFromUrl, loadStripe } from '../../utils/stripe';\nimport {\n STATUS,\n STATUS_LABELS,\n STRIPE_ELEMENT_OPTS,\n phaseFor,\n DEFAULTS,\n BILLING_COUNTRIES,\n} from '../../constants';\nimport type {\n Status, Phase, Cart, BillingInfo, CheckoutClient, StripeLike, StripeElementLike,\n StripeElementsLike, BillingPlan, CheckoutConsent, SupportedPaymentMethod,\n} from '../../types';\nimport {\n useCartStatusQuery, useSubscriptionIdQuery, useBillingPlansQuery,\n useActiveSubscriptionsQuery, useViaxSessionQuery, useCourseDetailQuery,\n} from './useCheckoutQueries';\nimport type { EdxSessionResult } from '../../utils/provision';\nimport { useCheckoutMutations } from './useCheckoutMutations';\n\n/**\n * Read ?course_run_key= from the current URL.\n * Avoid URLSearchParams — it treats bare `+` as a space, which would turn\n * HarvardX+CS50+X into \"HarvardX CS50 X\". decodeURIComponent keeps `+` intact.\n */\nfunction getCourseRunKeyFromUrl(): string | null {\n if (typeof window === 'undefined') return null;\n const match = /[?&]course_run_key=([^&]*)/.exec(window.location.search);\n if (!match) return null;\n try {\n // Preserve literal `+` (treat as %2B) then decode %XX sequences.\n return decodeURIComponent(match[1].replace(/\\+/g, '%2B')).trim() || null;\n } catch {\n return match[1].trim() || null;\n }\n}\n\nconst redirectBrowser = (url: string): void => {\n window.location.assign(url);\n};\n\nconst getInitialBillingCountry = (userCountry: string | null): string => {\n const normalized = userCountry?.trim().toUpperCase();\n if (normalized && BILLING_COUNTRIES.some(([countryCode]) => countryCode === normalized)) {\n return normalized;\n }\n return 'US';\n};\n\nexport interface UseCheckoutOptions {\n apiUrl?: string,\n edxSessionUrl?: string,\n stripePublishableKey?: string,\n onComplete?: (subscriptionBiId: string | null) => void,\n /** Country code resolved by CheckoutGate — used to show/hide the UK/CA extra checkbox. */\n userCountry?: string | null,\n /**\n * Course run key from the URL (e.g. course-v1:HarvardX+CS50+X).\n * When omitted, read from ?course_run_key=.\n */\n courseRunKey?: string | null,\n /** Override only for tests; production performs a full-page browser redirect. */\n redirectToUrl?: (url: string) => void,\n}\n\nexport interface UseCheckoutResult {\n phase: Phase,\n status: Status | null,\n statusLabel: string,\n stripeReady: boolean,\n /** True once the Stripe card Element iframe is fully initialised and interactive. */\n cardReady: boolean,\n notConfigured: boolean,\n showBanner: boolean,\n dismissBanner: () => void,\n error: string,\n clearError: () => void,\n cardError: string,\n lastComment: string,\n /** Full subscription details including card last 4 — available after SUBSCRIPTION_CREATED. */\n subscriptionDetails: import('../../types').SubscriptionDetails | null,\n billing: BillingInfo,\n setField: (field: keyof BillingInfo, value: string) => void,\n numberMountRef: Ref<HTMLDivElement>,\n expiryMountRef: Ref<HTMLDivElement>,\n cvcMountRef: Ref<HTMLDivElement>,\n submitting: boolean,\n /** True from Place order click until a terminal status is reached. */\n processingSubscription: boolean,\n /** True when Stripe sent the learner back with ?cart_bi_id=. */\n returningFromStripe: boolean,\n placeOrder: (consent: CheckoutConsent, paymentMethod?: SupportedPaymentMethod) => void,\n /** Billing plans fetched on mount — includes prices for display in CartSummary. */\n billingPlans: BillingPlan[],\n /** Live active subscriber count for display in CartSummary. */\n activeSubscriberCount: number | null,\n /** Currently selected billing cycle. */\n selectedCycle: BillingCycle,\n /** Switch between monthly and annual billing. */\n setCycle: (cycle: BillingCycle) => void,\n /** True once GET /edx-session has returned and the viax token is set. */\n sessionReady: boolean,\n /** ISO 3166-1 alpha-2 country code — passed in from CheckoutGate for the UK/CA checkbox. */\n userCountry: string | null,\n /** Course details when arrived via ?course_run_key=; null for standard checkout. */\n course: import('../../types').CourseDetail | null,\n}\n\nexport default function useCheckout(options: UseCheckoutOptions = {}): UseCheckoutResult {\n const {\n apiUrl = DEFAULTS.apiUrl,\n edxSessionUrl,\n stripePublishableKey,\n onComplete,\n userCountry = null,\n courseRunKey: courseRunKeyProp,\n redirectToUrl = redirectBrowser,\n } = options;\n\n const courseRunKey = useMemo(\n () => courseRunKeyProp ?? getCourseRunKeyFromUrl(),\n [courseRunKeyProp],\n );\n\n const client = useMemo<CheckoutClient | null>(() => {\n if (!apiUrl) return null;\n return createCheckoutClient({ apiUrl });\n }, [apiUrl]);\n\n const notConfigured = !client || !stripePublishableKey || !edxSessionUrl;\n\n // Exchange edX session for viax token on page load.\n const sessionQuery = useViaxSessionQuery(notConfigured ? null : edxSessionUrl!);\n const sessionReady = !!sessionQuery.data;\n\n useEffect(() => {\n if (sessionQuery.data && client) {\n client.setToken(sessionQuery.data.viaxToken);\n }\n }, [sessionQuery.data, client]);\n\n // Fetch billing plans, subscriber count, and optional course once the viax session is ready.\n const billingPlansQuery = useBillingPlansQuery(client, sessionReady);\n const activeSubscriptionsQuery = useActiveSubscriptionsQuery(client, sessionReady);\n const courseDetailQuery = useCourseDetailQuery(client, sessionReady, courseRunKey);\n\n // --- state ---\n const [showBanner, setShowBanner] = useState(true);\n const [cart, setCart] = useState<Cart | null>(null);\n const [stripe, setStripe] = useState<StripeLike | null>(null);\n const [stripeReady, setStripeReady] = useState(false);\n const [cardReady, setCardReady] = useState(false);\n const [error, setError] = useState('');\n const [cardError, setCardError] = useState('');\n const [billing, setBilling] = useState<BillingInfo>({\n firstName: '', lastName: '', city: '', zip: '', country: getInitialBillingCountry(userCountry), line1: '', state: '',\n });\n const [selectedCycle, setSelectedCycle] = useState<BillingCycle>(getPlanFromUrl);\n // Set when Stripe returns from a 3DS challenge; cleared once a new attempt starts.\n const [returnedCartBiId, setReturnedCartBiId] = useState<string | null>(getReturnedCartBiIdFromUrl);\n const [processingSubscription, setProcessingSubscription] = useState(\n () => getReturnedCartBiIdFromUrl() !== null,\n );\n\n const setCycle = useCallback((cycle: BillingCycle) => {\n setSelectedCycle(cycle);\n writePlanQueryParam(cycle);\n }, []);\n\n useEffect(() => {\n if (sessionQuery.error) {\n setError(sessionQuery.error.message);\n }\n }, [sessionQuery.error]);\n\n const setField = useCallback(\n (field: keyof BillingInfo, value: string) => setBilling((p: BillingInfo) => ({ ...p, [field]: value })),\n [],\n );\n\n // --- refs ---\n // Stripe instances — written inside useEffect and mutations.\n const stripeInstanceRef = useRef<StripeLike | null>(null);\n const cardNumberElementRef = useRef<StripeElementLike | null>(null);\n const cardExpiryElementRef = useRef<StripeElementLike | null>(null);\n const cardCvcElementRef = useRef<StripeElementLike | null>(null);\n const elementsRef = useRef<StripeElementsLike | null>(null);\n // DOM mount targets — callback refs so we remount when hosts leave and return.\n const numberMountNodeRef = useRef<HTMLDivElement | null>(null);\n const expiryMountNodeRef = useRef<HTMLDivElement | null>(null);\n const cvcMountNodeRef = useRef<HTMLDivElement | null>(null);\n const [cardHostsEpoch, setCardHostsEpoch] = useState(0);\n\n const assignCardHost = useCallback((\n store: { current: HTMLDivElement | null },\n node: HTMLDivElement | null,\n ) => {\n store.current = node;\n if (\n numberMountNodeRef.current\n && expiryMountNodeRef.current\n && cvcMountNodeRef.current\n ) {\n setCardHostsEpoch((n) => n + 1);\n return;\n }\n // Hosts are leaving the DOM — detach so Stripe is not bound to dead nodes.\n cardNumberElementRef.current?.unmount?.();\n cardExpiryElementRef.current?.unmount?.();\n cardCvcElementRef.current?.unmount?.();\n }, []);\n\n const numberMountRef = useCallback((node: HTMLDivElement | null) => {\n assignCardHost(numberMountNodeRef, node);\n }, [assignCardHost]);\n const expiryMountRef = useCallback((node: HTMLDivElement | null) => {\n assignCardHost(expiryMountNodeRef, node);\n }, [assignCardHost]);\n const cvcMountRef = useCallback((node: HTMLDivElement | null) => {\n assignCardHost(cvcMountNodeRef, node);\n }, [assignCardHost]);\n const mountedRef = useRef(true);\n const completedRef = useRef(false);\n const redirectedUrlRef = useRef<string | null>(null);\n const sessionRef = useRef<EdxSessionResult | null>(null);\n\n useEffect(() => {\n sessionRef.current = sessionQuery.data ?? null;\n }, [sessionQuery.data]);\n\n // Mirror state into refs so mutations can read current values without stale closures.\n const cartRef = useRef<Cart | null>(null);\n const billingRef = useRef<BillingInfo>(billing);\n const selectedCycleRef = useRef<BillingCycle>(selectedCycle);\n\n useEffect(() => {\n cartRef.current = cart;\n }, [cart]);\n useEffect(() => {\n billingRef.current = billing;\n }, [billing]);\n useEffect(() => {\n selectedCycleRef.current = selectedCycle;\n }, [selectedCycle]);\n useEffect(() => {\n stripeInstanceRef.current = stripe;\n }, [stripe]);\n\n // --- server state ---\n // Poll the cart created in this session, or the one Stripe just returned\n // from. The returned cart waits for the viax session so the status request\n // carries a valid token.\n const cartBiId = cart?.biId ?? (sessionReady ? returnedCartBiId : null);\n const statusQuery = useCartStatusQuery(client, cartBiId);\n const status: Status | null = statusQuery.data?.status ?? null;\n const lastComment = statusQuery.data?.lastComment ?? '';\n\n // Phase is 'ready' until a cart exists, then driven by bpStatus.\n const phase: Phase = cartBiId ? phaseFor(status, status === STATUS.IN_CART) : 'ready';\n const subscriptionIdQuery = useSubscriptionIdQuery(\n client, cartBiId, status === STATUS.SUBSCRIPTION_CREATED,\n );\n\n // --- mutations ---\n const { placeOrder: placeMutation } = useCheckoutMutations({\n client,\n sessionRef,\n stripeRef: stripeInstanceRef,\n cardNumberElRef: cardNumberElementRef,\n billingRef,\n selectedCycleRef,\n setError: (msg: string) => {\n setProcessingSubscription(false);\n setError(msg);\n },\n setCardError: (msg: string) => {\n setProcessingSubscription(false);\n setCardError(msg);\n },\n onCartCreated: (created: Cart) => {\n // Sync the ref immediately so any same-tick readers see the cart\n // before the setCart → useEffect mirror runs.\n cartRef.current = created;\n if (mountedRef.current) setCart(created);\n },\n });\n\n // --- load Stripe.js on mount ---\n useEffect(() => {\n mountedRef.current = true;\n if (!stripePublishableKey) return undefined;\n\n (async () => {\n try {\n const instance = await loadStripe(stripePublishableKey);\n if (mountedRef.current) {\n setStripe(instance);\n setStripeReady(true);\n }\n } catch (e: unknown) {\n if (mountedRef.current) setError((e as Error).message);\n }\n })();\n\n return () => {\n mountedRef.current = false;\n };\n }, [stripePublishableKey]);\n\n // --- mount Stripe Elements once Stripe is loaded; remount if hosts return ---\n useEffect(() => {\n if (!stripe) return;\n const numberNode = numberMountNodeRef.current;\n const expiryNode = expiryMountNodeRef.current;\n const cvcNode = cvcMountNodeRef.current;\n if (!numberNode || !expiryNode || !cvcNode) return;\n\n if (cardNumberElementRef.current && cardExpiryElementRef.current && cardCvcElementRef.current) {\n try {\n cardNumberElementRef.current.unmount?.();\n cardExpiryElementRef.current.unmount?.();\n cardCvcElementRef.current.unmount?.();\n cardNumberElementRef.current.mount(numberNode);\n cardExpiryElementRef.current.mount(expiryNode);\n cardCvcElementRef.current.mount(cvcNode);\n return;\n } catch {\n // Element is bound to a destroyed host — create a fresh Elements group.\n cardNumberElementRef.current = null;\n cardExpiryElementRef.current = null;\n cardCvcElementRef.current = null;\n elementsRef.current = null;\n }\n }\n\n setCardReady(false);\n\n const elements = stripe.elements();\n elementsRef.current = elements;\n\n const number = elements.create('cardNumber', { ...STRIPE_ELEMENT_OPTS, showIcon: true });\n const expiry = elements.create('cardExpiry', STRIPE_ELEMENT_OPTS);\n const cvc = elements.create('cardCvc', STRIPE_ELEMENT_OPTS);\n\n number.mount(numberNode);\n expiry.mount(expiryNode);\n cvc.mount(cvcNode);\n\n number.on('ready', () => {\n if (mountedRef.current) setCardReady(true);\n });\n number.on('change', (e: { error?: { message?: string } }) => {\n if (mountedRef.current) setCardError(e.error?.message || '');\n });\n\n cardNumberElementRef.current = number;\n cardExpiryElementRef.current = expiry;\n cardCvcElementRef.current = cvc;\n }, [stripe, cardHostsEpoch]);\n\n // --- on success ---\n useEffect(() => {\n if (status !== STATUS.SUBSCRIPTION_CREATED || completedRef.current) return;\n completedRef.current = true;\n if (onComplete) onComplete(subscriptionIdQuery.data?.biId ?? null);\n }, [status, subscriptionIdQuery.data, onComplete]);\n\n // Stripe and PayPal both authenticate the buyer on a hosted page before\n // returning the browser to checkout, so this must be a full-page\n // navigation (not an API request) for either flow.\n useEffect(() => {\n const redirectUrl = statusQuery.data?.stripeConfirmationRedirectUrl\n || statusQuery.data?.paypalApprovalUrl;\n if (\n status !== STATUS.STRIPE_CONFIRMATION_REQUIRED\n || !redirectUrl\n || redirectedUrlRef.current === redirectUrl\n ) return;\n\n redirectedUrlRef.current = redirectUrl;\n redirectToUrl(redirectUrl);\n }, [\n status,\n statusQuery.data?.stripeConfirmationRedirectUrl,\n statusQuery.data?.paypalApprovalUrl,\n redirectToUrl,\n ]);\n\n // --- actions ---\n const placeOrder = useCallback((consent: CheckoutConsent, paymentMethod: SupportedPaymentMethod = 'card') => {\n setError('');\n setCardError('');\n if (phase === 'failed') setCart(null);\n // Each attempt creates its own cart, so stop reporting on the returned one.\n setReturnedCartBiId(null);\n setProcessingSubscription(true);\n placeMutation.mutate({ consent, paymentMethod });\n }, [placeMutation, phase]);\n\n // Turn off processingSubscription once we reach a terminal status.\n useEffect(() => {\n if (!status) return;\n const terminalStatuses: Status[] = [\n STATUS.SUBSCRIPTION_CREATED,\n STATUS.STRIPE_FAIL,\n STATUS.SANCTION_BLOCK,\n ];\n if (terminalStatuses.includes(status)) {\n setProcessingSubscription(false);\n }\n }, [status]);\n\n return {\n phase,\n status,\n statusLabel: (status && STATUS_LABELS[status]) || 'Processing your order…',\n stripeReady,\n cardReady,\n notConfigured,\n showBanner,\n dismissBanner: () => setShowBanner(false),\n error,\n clearError: () => setError(''),\n cardError,\n lastComment,\n subscriptionDetails: subscriptionIdQuery.data ?? null,\n billing,\n setField,\n numberMountRef,\n expiryMountRef,\n cvcMountRef,\n submitting: placeMutation.isPending,\n processingSubscription,\n returningFromStripe: returnedCartBiId !== null,\n placeOrder,\n billingPlans: billingPlansQuery.data?.plans ?? [],\n activeSubscriberCount: activeSubscriptionsQuery.data?.active ?? null,\n selectedCycle,\n setCycle,\n sessionReady,\n userCountry,\n course: courseDetailQuery.data ?? null,\n };\n}\n"]}
@@ -0,0 +1,16 @@
1
+ export interface UseCheckoutGateOptions {
2
+ apiUrl?: string;
3
+ edxSessionUrl?: string;
4
+ learnerDashboardUrl?: string;
5
+ }
6
+ export type GateStatus = 'loading' | 'blocked' | 'allowed' | 'error' | 'existingSubscriber';
7
+ export interface UseCheckoutGateResult {
8
+ /** 'loading' while session/checks are in flight, 'blocked' if country not allowed, 'allowed' if clear, 'error' if session failed. */
9
+ gateStatus: GateStatus;
10
+ /** ISO 3166-1 alpha-2 country code — passed down to Checkout for the UK/CA checkbox. */
11
+ userCountry: string | null;
12
+ learnerDashboardUrl: string | undefined;
13
+ /** Message shown when gateStatus is not 'allowed'. */
14
+ gateMessage: string | null;
15
+ }
16
+ export declare function useCheckoutGate(options?: UseCheckoutGateOptions): UseCheckoutGateResult;
@@ -0,0 +1,61 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import { ALLOWED_CHECKOUT_COUNTRIES, GATE_CHECK_COUNTRY, } from '../../constants';
3
+ import { createCheckoutClient } from '../../utils/checkout';
4
+ import { getLearnerFacingCheckoutMessage } from '../../utils/checkoutErrors';
5
+ import { useCurrentUserSubscriberQuery, useViaxSessionQuery, useUserCountryQuery, } from './useCheckoutQueries';
6
+ export function useCheckoutGate(options = {}) {
7
+ var _a, _b, _c, _d;
8
+ const { apiUrl, edxSessionUrl, learnerDashboardUrl, } = options;
9
+ const client = useMemo(() => (apiUrl ? createCheckoutClient({ apiUrl }) : null), [apiUrl]);
10
+ const sessionQuery = useViaxSessionQuery(edxSessionUrl !== null && edxSessionUrl !== void 0 ? edxSessionUrl : null);
11
+ const sessionReady = !!sessionQuery.data;
12
+ useEffect(() => {
13
+ if (sessionQuery.data && client) {
14
+ client.setToken(sessionQuery.data.viaxToken);
15
+ }
16
+ }, [sessionQuery.data, client]);
17
+ const currentUserSubscriberQuery = useCurrentUserSubscriberQuery(client, sessionReady);
18
+ const userCountryQuery = useUserCountryQuery(sessionReady && GATE_CHECK_COUNTRY ? (edxSessionUrl !== null && edxSessionUrl !== void 0 ? edxSessionUrl : null) : null, (_b = (_a = sessionQuery.data) === null || _a === void 0 ? void 0 : _a.username) !== null && _b !== void 0 ? _b : null);
19
+ const userCountry = (_c = userCountryQuery.data) !== null && _c !== void 0 ? _c : null;
20
+ const isExistingSubscriber = ((_d = currentUserSubscriberQuery.data) === null || _d === void 0 ? void 0 : _d.isSubscriber) === true;
21
+ // Use isLoading (isPending && isFetching), not isPending alone.
22
+ // In TanStack Query v5, disabled queries with no data stay isPending forever —
23
+ // e.g. country check when username is missing from the session response.
24
+ const loading = (!sessionReady && !sessionQuery.isError)
25
+ || currentUserSubscriberQuery.isLoading
26
+ || (GATE_CHECK_COUNTRY && userCountryQuery.isLoading);
27
+ // GATE_CHECK_COUNTRY=false → treat all countries as allowed.
28
+ const isCountryAllowed = !GATE_CHECK_COUNTRY
29
+ || userCountry === null
30
+ || ALLOWED_CHECKOUT_COUNTRIES.includes(userCountry);
31
+ let gateStatus;
32
+ if (sessionQuery.isError) {
33
+ gateStatus = 'error';
34
+ }
35
+ else if (loading) {
36
+ gateStatus = 'loading';
37
+ }
38
+ else if (isExistingSubscriber) {
39
+ gateStatus = 'existingSubscriber';
40
+ }
41
+ else if (!isCountryAllowed) {
42
+ gateStatus = 'blocked';
43
+ }
44
+ else {
45
+ gateStatus = 'allowed';
46
+ }
47
+ const gateMessage = gateStatus === 'error'
48
+ ? (sessionQuery.error instanceof Error
49
+ ? sessionQuery.error.message
50
+ : 'Failed to establish checkout session.')
51
+ : gateStatus === 'existingSubscriber'
52
+ ? getLearnerFacingCheckoutMessage('duplicate_active_subscription')
53
+ : null;
54
+ return {
55
+ gateStatus,
56
+ userCountry,
57
+ learnerDashboardUrl,
58
+ gateMessage,
59
+ };
60
+ }
61
+ //# sourceMappingURL=useCheckoutGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCheckoutGate.js","sourceRoot":"","sources":["../../../src/data/hooks/useCheckoutGate.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo } from 'react';\nimport {\n ALLOWED_CHECKOUT_COUNTRIES,\n GATE_CHECK_COUNTRY,\n} from '../../constants';\nimport { createCheckoutClient } from '../../utils/checkout';\nimport { getLearnerFacingCheckoutMessage } from '../../utils/checkoutErrors';\nimport {\n useCurrentUserSubscriberQuery,\n useViaxSessionQuery,\n useUserCountryQuery,\n} from './useCheckoutQueries';\n\nexport interface UseCheckoutGateOptions {\n apiUrl?: string,\n edxSessionUrl?: string,\n learnerDashboardUrl?: string,\n}\n\nexport type GateStatus = 'loading' | 'blocked' | 'allowed' | 'error' | 'existingSubscriber';\n\nexport interface UseCheckoutGateResult {\n /** 'loading' while session/checks are in flight, 'blocked' if country not allowed, 'allowed' if clear, 'error' if session failed. */\n gateStatus: GateStatus,\n /** ISO 3166-1 alpha-2 country code — passed down to Checkout for the UK/CA checkbox. */\n userCountry: string | null,\n learnerDashboardUrl: string | undefined,\n /** Message shown when gateStatus is not 'allowed'. */\n gateMessage: string | null,\n}\n\nexport function useCheckoutGate(options: UseCheckoutGateOptions = {}): UseCheckoutGateResult {\n const {\n apiUrl,\n edxSessionUrl,\n learnerDashboardUrl,\n } = options;\n\n const client = useMemo(\n () => (apiUrl ? createCheckoutClient({ apiUrl }) : null),\n [apiUrl],\n );\n const sessionQuery = useViaxSessionQuery(edxSessionUrl ?? null);\n const sessionReady = !!sessionQuery.data;\n\n useEffect(() => {\n if (sessionQuery.data && client) {\n client.setToken(sessionQuery.data.viaxToken);\n }\n }, [sessionQuery.data, client]);\n\n const currentUserSubscriberQuery = useCurrentUserSubscriberQuery(client, sessionReady);\n const userCountryQuery = useUserCountryQuery(\n sessionReady && GATE_CHECK_COUNTRY ? (edxSessionUrl ?? null) : null,\n sessionQuery.data?.username ?? null,\n );\n\n const userCountry = userCountryQuery.data ?? null;\n const isExistingSubscriber = currentUserSubscriberQuery.data?.isSubscriber === true;\n\n // Use isLoading (isPending && isFetching), not isPending alone.\n // In TanStack Query v5, disabled queries with no data stay isPending forever —\n // e.g. country check when username is missing from the session response.\n const loading = (!sessionReady && !sessionQuery.isError)\n || currentUserSubscriberQuery.isLoading\n || (GATE_CHECK_COUNTRY && userCountryQuery.isLoading);\n\n // GATE_CHECK_COUNTRY=false → treat all countries as allowed.\n const isCountryAllowed = !GATE_CHECK_COUNTRY\n || userCountry === null\n || ALLOWED_CHECKOUT_COUNTRIES.includes(userCountry);\n\n let gateStatus: GateStatus;\n if (sessionQuery.isError) {\n gateStatus = 'error';\n } else if (loading) {\n gateStatus = 'loading';\n } else if (isExistingSubscriber) {\n gateStatus = 'existingSubscriber';\n } else if (!isCountryAllowed) {\n gateStatus = 'blocked';\n } else {\n gateStatus = 'allowed';\n }\n\n const gateMessage = gateStatus === 'error'\n ? (sessionQuery.error instanceof Error\n ? sessionQuery.error.message\n : 'Failed to establish checkout session.')\n : gateStatus === 'existingSubscriber'\n ? getLearnerFacingCheckoutMessage('duplicate_active_subscription')\n : null;\n\n return {\n gateStatus,\n userCountry,\n learnerDashboardUrl,\n gateMessage,\n };\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { RefObject } from 'react';
2
+ import type { EdxSessionResult } from '../../utils/provision';
3
+ import type { CheckoutClient, StripeLike, StripeElementLike, Cart, BillingInfo, CheckoutConsent, BillingCycle, SupportedPaymentMethod } from '../../types';
4
+ export interface CheckoutMutationsCtx {
5
+ client: CheckoutClient | null;
6
+ sessionRef: RefObject<EdxSessionResult | null>;
7
+ stripeRef: RefObject<StripeLike | null>;
8
+ cardNumberElRef: RefObject<StripeElementLike | null>;
9
+ billingRef: RefObject<BillingInfo | null>;
10
+ /** Selected monthly/annual cycle at Place order time. */
11
+ selectedCycleRef: RefObject<BillingCycle>;
12
+ setError: (msg: string) => void;
13
+ setCardError: (msg: string) => void;
14
+ onCartCreated: (cart: Cart) => void;
15
+ }
16
+ export interface PlaceOrderArgs {
17
+ consent: CheckoutConsent;
18
+ paymentMethod: SupportedPaymentMethod;
19
+ }
20
+ export declare function useCheckoutMutations(ctx: CheckoutMutationsCtx): {
21
+ placeOrder: import("@tanstack/react-query").UseMutationResult<Cart, Error, PlaceOrderArgs, unknown>;
22
+ };