@edx/frontend-app-subscription-manage 1.1.0 → 1.2.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.
- package/dist/Main.js +1 -1
- package/dist/Main.js.map +1 -1
- package/dist/app.js +3 -0
- package/dist/app.js.map +1 -1
- package/dist/checkoutPage/CheckoutPage.d.ts +3 -0
- package/dist/checkoutPage/CheckoutPage.js +24 -0
- package/dist/checkoutPage/CheckoutPage.js.map +1 -0
- package/dist/components/BrandMarks/BrandMarks.d.ts +3 -0
- package/dist/components/BrandMarks/BrandMarks.js +9 -0
- package/dist/components/BrandMarks/BrandMarks.js.map +1 -0
- package/dist/components/CartSummary/CartSummary.d.ts +21 -0
- package/dist/components/CartSummary/CartSummary.js +30 -0
- package/dist/components/CartSummary/CartSummary.js.map +1 -0
- package/dist/components/CartSummary/Messages.d.ts +73 -0
- package/dist/components/CartSummary/Messages.js +76 -0
- package/dist/components/CartSummary/Messages.js.map +1 -0
- package/dist/components/Checkout/Checkout.d.ts +4 -0
- package/dist/components/Checkout/Checkout.js +37 -0
- package/dist/components/Checkout/Checkout.js.map +1 -0
- package/dist/components/CheckoutGate/CheckoutGate.d.ts +9 -0
- package/dist/components/CheckoutGate/CheckoutGate.js +34 -0
- package/dist/components/CheckoutGate/CheckoutGate.js.map +1 -0
- package/dist/components/CourseEnrollmentCard/CourseEnrollmentCard.d.ts +9 -0
- package/dist/components/CourseEnrollmentCard/CourseEnrollmentCard.js +28 -0
- package/dist/components/CourseEnrollmentCard/CourseEnrollmentCard.js.map +1 -0
- package/dist/components/CourseEnrollmentCard/Messages.d.ts +33 -0
- package/dist/components/CourseEnrollmentCard/Messages.js +35 -0
- package/dist/components/CourseEnrollmentCard/Messages.js.map +1 -0
- package/dist/components/EdxUnlimitedBanner/EdxUnlimitedBanner.d.ts +13 -0
- package/dist/components/EdxUnlimitedBanner/EdxUnlimitedBanner.js +11 -0
- package/dist/components/EdxUnlimitedBanner/EdxUnlimitedBanner.js.map +1 -0
- package/dist/components/EnrollmentSummary/EnrollmentSummary.d.ts +15 -0
- package/dist/components/EnrollmentSummary/EnrollmentSummary.js +21 -0
- package/dist/components/EnrollmentSummary/EnrollmentSummary.js.map +1 -0
- package/dist/components/EnrollmentSummary/Messages.d.ts +33 -0
- package/dist/components/EnrollmentSummary/Messages.js +35 -0
- package/dist/components/EnrollmentSummary/Messages.js.map +1 -0
- package/dist/components/PaymentFields/PaymentFields.d.ts +9 -0
- package/dist/components/PaymentFields/PaymentFields.js +7 -0
- package/dist/components/PaymentFields/PaymentFields.js.map +1 -0
- package/dist/components/PaymentForm/Messages.d.ts +88 -0
- package/dist/components/PaymentForm/Messages.js +95 -0
- package/dist/components/PaymentForm/Messages.js.map +1 -0
- package/dist/components/PaymentForm/PaymentForm.d.ts +21 -0
- package/dist/components/PaymentForm/PaymentForm.js +69 -0
- package/dist/components/PaymentForm/PaymentForm.js.map +1 -0
- package/dist/components/StatusScreens/Messages.d.ts +103 -0
- package/dist/components/StatusScreens/Messages.js +108 -0
- package/dist/components/StatusScreens/Messages.js.map +1 -0
- package/dist/components/StatusScreens/StatusScreens.d.ts +23 -0
- package/dist/components/StatusScreens/StatusScreens.js +25 -0
- package/dist/components/StatusScreens/StatusScreens.js.map +1 -0
- package/dist/constants.d.ts +59 -0
- package/dist/constants.js +275 -0
- package/dist/constants.js.map +1 -1
- package/dist/data/hooks/keys.d.ts +14 -0
- package/dist/data/hooks/keys.js +15 -0
- package/dist/data/hooks/keys.js.map +1 -0
- package/dist/data/hooks/useCheckout.d.ts +57 -0
- package/dist/data/hooks/useCheckout.js +249 -0
- package/dist/data/hooks/useCheckout.js.map +1 -0
- package/dist/data/hooks/useCheckoutGate.d.ts +15 -0
- package/dist/data/hooks/useCheckoutGate.js +54 -0
- package/dist/data/hooks/useCheckoutGate.js.map +1 -0
- package/dist/data/hooks/useCheckoutMutations.d.ts +16 -0
- package/dist/data/hooks/useCheckoutMutations.js +131 -0
- package/dist/data/hooks/useCheckoutMutations.js.map +1 -0
- package/dist/data/hooks/useCheckoutQueries.d.ts +35 -0
- package/dist/data/hooks/useCheckoutQueries.js +110 -0
- package/dist/data/hooks/useCheckoutQueries.js.map +1 -0
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +7 -5
- package/dist/routes.js.map +1 -1
- package/dist/style.scss +13 -0
- package/dist/styles/components/brandMarks.scss +54 -0
- package/dist/styles/components/cartSummary.scss +18 -0
- package/dist/styles/components/checkout.scss +29 -0
- package/dist/styles/components/checkoutStepper.scss +9 -0
- package/dist/styles/components/courseEnrollmentCard.scss +38 -0
- package/dist/styles/components/edxUnlimitedBanner.scss +138 -0
- package/dist/styles/components/enrollmentSummary.scss +13 -0
- package/dist/styles/components/paymentFields.scss +15 -0
- package/dist/styles/components/paymentForm.scss +105 -0
- package/dist/styles/components/statusScreens.scss +29 -0
- package/dist/styles/components/successScreen.scss +0 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/auth.d.ts +7 -0
- package/dist/utils/auth.js +28 -0
- package/dist/utils/auth.js.map +1 -0
- package/dist/utils/billingDetails.d.ts +30 -0
- package/dist/utils/billingDetails.js +47 -0
- package/dist/utils/billingDetails.js.map +1 -0
- package/dist/utils/bootstrapApi.d.ts +20 -0
- package/dist/utils/bootstrapApi.js +142 -0
- package/dist/utils/bootstrapApi.js.map +1 -0
- package/dist/utils/cartApi.d.ts +16 -0
- package/dist/utils/cartApi.js +118 -0
- package/dist/utils/cartApi.js.map +1 -0
- package/dist/utils/checkout.d.ts +6 -0
- package/dist/utils/checkout.js +28 -0
- package/dist/utils/checkout.js.map +1 -0
- package/dist/utils/fetchActiveSubscriptions.d.ts +4 -0
- package/dist/utils/fetchActiveSubscriptions.js +36 -0
- package/dist/utils/fetchActiveSubscriptions.js.map +1 -0
- package/dist/utils/fetchBillingPlans.d.ts +2 -0
- package/dist/utils/fetchBillingPlans.js +48 -0
- package/dist/utils/fetchBillingPlans.js.map +1 -0
- package/dist/utils/graphqlClient.d.ts +16 -0
- package/dist/utils/graphqlClient.js +39 -0
- package/dist/utils/graphqlClient.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +17 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/provision.d.ts +25 -0
- package/dist/utils/provision.js +107 -0
- package/dist/utils/provision.js.map +1 -0
- package/dist/utils/queries.d.ts +35 -0
- package/dist/utils/queries.js +168 -0
- package/dist/utils/queries.js.map +1 -0
- package/dist/utils/stripe.d.ts +7 -0
- package/dist/utils/stripe.js +43 -0
- package/dist/utils/stripe.js.map +1 -0
- package/package.json +2 -2
- package/dist/example/ExamplePage.d.ts +0 -2
- package/dist/example/ExamplePage.js +0 -6
- package/dist/example/ExamplePage.js.map +0 -1
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
/**
|
|
11
|
+
* query/useCheckoutMutations.ts — write-side TanStack mutations.
|
|
12
|
+
*
|
|
13
|
+
* The placeOrder mutation drives the checkout sequence on button click.
|
|
14
|
+
* The viax token and customerUid are established on page load via
|
|
15
|
+
* GET /edx-session and passed in through sessionRef.
|
|
16
|
+
*
|
|
17
|
+
* 1. getSalableUid + getBillingPlans (parallel)
|
|
18
|
+
* 2. createTwouCart
|
|
19
|
+
* 3. stripe.createPaymentMethod → pm_xxx
|
|
20
|
+
* 4. updateTwouCartItem → billing address + pm_xxx
|
|
21
|
+
* 5. updateTwouCart → consent fields (SUBS-228)
|
|
22
|
+
* 6. startBusinessProcessableTransition('Sanction_Check')
|
|
23
|
+
*/
|
|
24
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
25
|
+
import { STATUS } from '../../constants';
|
|
26
|
+
import { checkoutKeys } from './keys';
|
|
27
|
+
class CardValidationError extends Error {
|
|
28
|
+
}
|
|
29
|
+
function toCartAddress(b) {
|
|
30
|
+
return {
|
|
31
|
+
tFirstName: b.firstName,
|
|
32
|
+
tLastName: b.lastName,
|
|
33
|
+
upaLine1: b.line1,
|
|
34
|
+
upaCity: b.city,
|
|
35
|
+
upaPostcode: b.zip,
|
|
36
|
+
gadStateOrProvince: b.state,
|
|
37
|
+
gadCountry: { iso2Code: b.country },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toStripeAddress(b) {
|
|
41
|
+
return {
|
|
42
|
+
line1: b.line1 || undefined,
|
|
43
|
+
city: b.city || undefined,
|
|
44
|
+
state: b.state || undefined,
|
|
45
|
+
postal_code: b.zip || undefined,
|
|
46
|
+
country: b.country || undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function useCheckoutMutations(ctx) {
|
|
50
|
+
const queryClient = useQueryClient();
|
|
51
|
+
const optimisticConfirm = (cartBiId) => {
|
|
52
|
+
queryClient.setQueryData(checkoutKeys.status(cartBiId), (old) => {
|
|
53
|
+
var _a, _b;
|
|
54
|
+
return ({
|
|
55
|
+
status: STATUS.CONFIRM,
|
|
56
|
+
stripeSubscriptionId: (_a = old === null || old === void 0 ? void 0 : old.stripeSubscriptionId) !== null && _a !== void 0 ? _a : null,
|
|
57
|
+
lastComment: (_b = old === null || old === void 0 ? void 0 : old.lastComment) !== null && _b !== void 0 ? _b : '',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
queryClient.invalidateQueries({ queryKey: checkoutKeys.status(cartBiId) });
|
|
61
|
+
};
|
|
62
|
+
const placeOrder = useMutation({
|
|
63
|
+
mutationFn: (consent) => __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
var _a;
|
|
65
|
+
const { client, stripeRef, cardNumberElRef, billingRef } = ctx;
|
|
66
|
+
const stripe = stripeRef.current;
|
|
67
|
+
const card = cardNumberElRef.current;
|
|
68
|
+
if (!client || !stripe || !card)
|
|
69
|
+
throw new Error('Checkout is not ready yet.');
|
|
70
|
+
const billing = billingRef.current;
|
|
71
|
+
if (!billing)
|
|
72
|
+
throw new Error('Billing information is not available. Please refresh and try again.');
|
|
73
|
+
const session = ctx.sessionRef.current;
|
|
74
|
+
if (!session) {
|
|
75
|
+
throw new Error('Checkout session is not ready yet. Please wait a moment and try again.');
|
|
76
|
+
}
|
|
77
|
+
const { customerUid } = session;
|
|
78
|
+
// ── Step 1: Fetch salable(product) uid + billing plans in parallel ─────────────
|
|
79
|
+
const [productInfo, plansResult] = yield Promise.all([
|
|
80
|
+
client.getSalableUid('TWOU-SUB-PLAN-001'),
|
|
81
|
+
client.getBillingPlans(),
|
|
82
|
+
]);
|
|
83
|
+
const salableUid = productInfo.uid;
|
|
84
|
+
const defaultPlan = (_a = plansResult.plans.find((p) => !p.isTemplate)) !== null && _a !== void 0 ? _a : plansResult.plans[0];
|
|
85
|
+
if (!defaultPlan)
|
|
86
|
+
throw new Error('No billing plans available.');
|
|
87
|
+
const billingPlanUid = defaultPlan.uid;
|
|
88
|
+
// ── Step 2: Create the cart ───────────────────────────────────────────
|
|
89
|
+
const createdCart = yield client.createCart({ customerUid, salableUid, billingPlanUid });
|
|
90
|
+
ctx.onCartCreated(createdCart);
|
|
91
|
+
// ── Step 3: Tokenize the card (browser → Stripe) ──────────────────────
|
|
92
|
+
const { paymentMethod, error } = yield stripe.createPaymentMethod({
|
|
93
|
+
type: 'card',
|
|
94
|
+
card,
|
|
95
|
+
billing_details: {
|
|
96
|
+
name: `${billing.firstName} ${billing.lastName}`.trim(),
|
|
97
|
+
address: toStripeAddress(billing),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (error || !paymentMethod) {
|
|
101
|
+
throw new CardValidationError((error === null || error === void 0 ? void 0 : error.message) || 'There was a problem with your card.');
|
|
102
|
+
}
|
|
103
|
+
// ── Step 4: Attach billing address + payment method to cart item ──────
|
|
104
|
+
const { cartItemUid } = yield client.getCartItem(createdCart.biId);
|
|
105
|
+
yield client.updateCartItem({
|
|
106
|
+
cartItemUid,
|
|
107
|
+
billingAddress: toCartAddress(billing),
|
|
108
|
+
paymentMethodId: paymentMethod.id,
|
|
109
|
+
});
|
|
110
|
+
// ── Step 5: Persist auto-renewal consent on the TwouCart BI ───────────
|
|
111
|
+
yield client.updateCartConsent({
|
|
112
|
+
cartUid: createdCart.uid,
|
|
113
|
+
consent,
|
|
114
|
+
});
|
|
115
|
+
// ── Step 6: Fire the backend workflow ─────────────────────────────────
|
|
116
|
+
yield client.placeOrder(createdCart.uid);
|
|
117
|
+
return createdCart;
|
|
118
|
+
}),
|
|
119
|
+
onSuccess: (cart) => {
|
|
120
|
+
optimisticConfirm(cart.biId);
|
|
121
|
+
},
|
|
122
|
+
onError: (err) => {
|
|
123
|
+
if (err instanceof CardValidationError)
|
|
124
|
+
ctx.setCardError(err.message);
|
|
125
|
+
else
|
|
126
|
+
ctx.setError(err.message);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
return { placeOrder };
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=useCheckoutMutations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCheckoutMutations.js","sourceRoot":"","sources":["../../../src/data/hooks/useCheckoutMutations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * query/useCheckoutMutations.ts — write-side TanStack mutations.\n *\n * The placeOrder mutation drives the checkout sequence on button click.\n * The viax token and customerUid are established on page load via\n * GET /edx-session and passed in through sessionRef.\n *\n * 1. getSalableUid + getBillingPlans (parallel)\n * 2. createTwouCart\n * 3. stripe.createPaymentMethod → pm_xxx\n * 4. updateTwouCartItem → billing address + pm_xxx\n * 5. updateTwouCart → consent fields (SUBS-228)\n * 6. startBusinessProcessableTransition('Sanction_Check')\n */\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport type { RefObject } from 'react';\nimport { STATUS } from '../../constants';\nimport { checkoutKeys } from './keys';\nimport type { EdxSessionResult } from '../../utils/provision';\nimport type {\n CheckoutClient, StripeLike, StripeElementLike, Cart, BillingInfo,\n CartBillingAddress, CartStatusResult, CheckoutConsent,\n} from '../../types';\n\nclass CardValidationError extends Error {}\n\nfunction toCartAddress(b: BillingInfo): CartBillingAddress {\n return {\n tFirstName: b.firstName,\n tLastName: b.lastName,\n upaLine1: b.line1,\n upaCity: b.city,\n upaPostcode: b.zip,\n gadStateOrProvince: b.state,\n gadCountry: { iso2Code: b.country },\n };\n}\n\nfunction toStripeAddress(b: BillingInfo): Record<string, string | undefined> {\n return {\n line1: b.line1 || undefined,\n city: b.city || undefined,\n state: b.state || undefined,\n postal_code: b.zip || undefined,\n country: b.country || undefined,\n };\n}\n\nexport interface CheckoutMutationsCtx {\n client: CheckoutClient | null,\n sessionRef: RefObject<EdxSessionResult | null>,\n stripeRef: RefObject<StripeLike | null>,\n cardNumberElRef: RefObject<StripeElementLike | null>,\n billingRef: RefObject<BillingInfo | null>,\n setError: (msg: string) => void,\n setCardError: (msg: string) => void,\n onCartCreated: (cart: Cart) => void,\n}\n\nexport function useCheckoutMutations(ctx: CheckoutMutationsCtx) {\n const queryClient = useQueryClient();\n\n const optimisticConfirm = (cartBiId: string) => {\n queryClient.setQueryData<CartStatusResult>(checkoutKeys.status(cartBiId), (old) => ({\n status: STATUS.CONFIRM,\n stripeSubscriptionId: old?.stripeSubscriptionId ?? null,\n lastComment: old?.lastComment ?? '',\n }));\n queryClient.invalidateQueries({ queryKey: checkoutKeys.status(cartBiId) });\n };\n\n const placeOrder = useMutation<Cart, Error, CheckoutConsent>({\n mutationFn: async (consent) => {\n const { client, stripeRef, cardNumberElRef, billingRef } = ctx;\n const stripe = stripeRef.current;\n const card = cardNumberElRef.current;\n if (!client || !stripe || !card) throw new Error('Checkout is not ready yet.');\n\n const billing = billingRef.current;\n if (!billing) throw new Error('Billing information is not available. Please refresh and try again.');\n\n const session = ctx.sessionRef.current;\n if (!session) {\n throw new Error('Checkout session is not ready yet. Please wait a moment and try again.');\n }\n const { customerUid } = session;\n\n // ── Step 1: Fetch salable(product) uid + billing plans in parallel ─────────────\n const [productInfo, plansResult] = await Promise.all([\n client.getSalableUid('TWOU-SUB-PLAN-001'),\n client.getBillingPlans(),\n ]);\n const salableUid = productInfo.uid;\n const defaultPlan = plansResult.plans.find((p) => !p.isTemplate) ?? plansResult.plans[0];\n if (!defaultPlan) throw new Error('No billing plans available.');\n const billingPlanUid = defaultPlan.uid;\n\n // ── Step 2: Create the cart ───────────────────────────────────────────\n const createdCart = await client.createCart({ customerUid, salableUid, billingPlanUid });\n ctx.onCartCreated(createdCart);\n\n // ── Step 3: Tokenize the card (browser → Stripe) ──────────────────────\n const { paymentMethod, error } = await stripe.createPaymentMethod({\n type: 'card',\n card,\n billing_details: {\n name: `${billing.firstName} ${billing.lastName}`.trim(),\n address: toStripeAddress(billing),\n },\n });\n if (error || !paymentMethod) {\n throw new CardValidationError(error?.message || 'There was a problem with your card.');\n }\n\n // ── Step 4: Attach billing address + payment method to cart item ──────\n const { cartItemUid } = await client.getCartItem(createdCart.biId);\n await client.updateCartItem({\n cartItemUid,\n billingAddress: toCartAddress(billing),\n paymentMethodId: paymentMethod.id,\n });\n\n // ── Step 5: Persist auto-renewal consent on the TwouCart BI ───────────\n await client.updateCartConsent({\n cartUid: createdCart.uid,\n consent,\n });\n\n // ── Step 6: Fire the backend workflow ─────────────────────────────────\n await client.placeOrder(createdCart.uid);\n\n return createdCart;\n },\n onSuccess: (cart) => {\n optimisticConfirm(cart.biId);\n },\n onError: (err) => {\n if (err instanceof CardValidationError) ctx.setCardError(err.message);\n else ctx.setError(err.message);\n },\n });\n\n return { placeOrder };\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CheckoutClient, CartStatusResult, BillingPlansResult, CourseDetail } from '../../types';
|
|
2
|
+
/** Exchange edX session cookies for a viax token on page load. */
|
|
3
|
+
export declare function useViaxSessionQuery(sessionUrl: string | null): import("@tanstack/react-query").UseQueryResult<import("../../utils/provision").EdxSessionResult, Error>;
|
|
4
|
+
/**
|
|
5
|
+
* Fetches billing plans after the viax session is established.
|
|
6
|
+
* Used to display prices in CartSummary before the user starts checkout.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useBillingPlansQuery(client: CheckoutClient | null, sessionReady: boolean): import("@tanstack/react-query").UseQueryResult<BillingPlansResult, Error>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetches the live active subscriber count after the viax session is ready.
|
|
11
|
+
* Refreshes every 5 minutes so the number stays roughly current.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useActiveSubscriptionsQuery(client: CheckoutClient | null, sessionReady: boolean): import("@tanstack/react-query").UseQueryResult<{
|
|
14
|
+
active: number;
|
|
15
|
+
}, Error>;
|
|
16
|
+
/**
|
|
17
|
+
* Fetches the user's country from their edX account profile.
|
|
18
|
+
* Derives the LMS base URL from the session URL (same origin).
|
|
19
|
+
* Returns null if the fetch fails or the field is absent — checkout stays open in that case.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useUserCountryQuery(sessionUrl: string | null, username: string | null): import("@tanstack/react-query").UseQueryResult<string | null, Error>;
|
|
22
|
+
/**
|
|
23
|
+
* Checks whether the current user already has an active subscription.
|
|
24
|
+
* Requires a viax token (session must be ready). Runs once on mount.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useIsSubscriberQuery(client: CheckoutClient | null, sessionReady: boolean): import("@tanstack/react-query").UseQueryResult<import("../../types").CurrentUserSubscriberResult | null, Error>;
|
|
27
|
+
/** Polls the cart state machine every second; stops on terminal states. */
|
|
28
|
+
export declare function useCartStatusQuery(client: CheckoutClient | null, cartBiId: string | null): import("@tanstack/react-query").UseQueryResult<CartStatusResult, Error>;
|
|
29
|
+
/** Looks up the created subscription biId after success. */
|
|
30
|
+
export declare function useSubscriptionIdQuery(client: CheckoutClient | null, cartBiId: string | null, enabled: boolean): import("@tanstack/react-query").UseQueryResult<import("../../types").SubscriptionDetails | null, Error>;
|
|
31
|
+
/**
|
|
32
|
+
* Fetches course details when the user arrived via ?course_run_key=.
|
|
33
|
+
* Skipped entirely when no course key is present (standard checkout).
|
|
34
|
+
*/
|
|
35
|
+
export declare function useCourseDetailQuery(client: CheckoutClient | null, sessionReady: boolean, courseRunKey: string | null): import("@tanstack/react-query").UseQueryResult<CourseDetail | null, Error>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* query/useCheckoutQueries.ts — read-side TanStack Query hooks.
|
|
3
|
+
*
|
|
4
|
+
* useViaxSessionQuery runs on mount to exchange the edX session for a viax
|
|
5
|
+
* token. Billing plans and subscriber count wait until the session is ready.
|
|
6
|
+
*/
|
|
7
|
+
import { useQuery } from '@tanstack/react-query';
|
|
8
|
+
import { isTerminalStatus } from '../../constants';
|
|
9
|
+
import { checkoutKeys } from './keys';
|
|
10
|
+
import { fetchEdxSession, fetchUserCountry } from '../../utils/provision';
|
|
11
|
+
/** Exchange edX session cookies for a viax token on page load. */
|
|
12
|
+
export function useViaxSessionQuery(sessionUrl) {
|
|
13
|
+
return useQuery({
|
|
14
|
+
queryKey: checkoutKeys.viaxSession(),
|
|
15
|
+
enabled: !!sessionUrl,
|
|
16
|
+
queryFn: () => fetchEdxSession(sessionUrl),
|
|
17
|
+
staleTime: 5 * 60 * 1000,
|
|
18
|
+
retry: 1,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Fetches billing plans after the viax session is established.
|
|
23
|
+
* Used to display prices in CartSummary before the user starts checkout.
|
|
24
|
+
*/
|
|
25
|
+
export function useBillingPlansQuery(client, sessionReady) {
|
|
26
|
+
return useQuery({
|
|
27
|
+
queryKey: checkoutKeys.billingPlans(),
|
|
28
|
+
enabled: !!client && sessionReady,
|
|
29
|
+
queryFn: () => client.getBillingPlans(),
|
|
30
|
+
staleTime: Infinity,
|
|
31
|
+
retry: 2,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Fetches the live active subscriber count after the viax session is ready.
|
|
36
|
+
* Refreshes every 5 minutes so the number stays roughly current.
|
|
37
|
+
*/
|
|
38
|
+
export function useActiveSubscriptionsQuery(client, sessionReady) {
|
|
39
|
+
return useQuery({
|
|
40
|
+
queryKey: ['checkout', 'activeSubscriptions'],
|
|
41
|
+
enabled: !!client && sessionReady,
|
|
42
|
+
queryFn: () => client.getActiveSubscriptions(),
|
|
43
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
44
|
+
retry: 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fetches the user's country from their edX account profile.
|
|
49
|
+
* Derives the LMS base URL from the session URL (same origin).
|
|
50
|
+
* Returns null if the fetch fails or the field is absent — checkout stays open in that case.
|
|
51
|
+
*/
|
|
52
|
+
export function useUserCountryQuery(sessionUrl, username) {
|
|
53
|
+
return useQuery({
|
|
54
|
+
queryKey: ['checkout', 'userCountry', username],
|
|
55
|
+
enabled: !!sessionUrl && !!username,
|
|
56
|
+
queryFn: () => {
|
|
57
|
+
const lmsBaseUrl = new URL(sessionUrl).origin;
|
|
58
|
+
return fetchUserCountry(lmsBaseUrl, username);
|
|
59
|
+
},
|
|
60
|
+
staleTime: Infinity,
|
|
61
|
+
retry: 1,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Checks whether the current user already has an active subscription.
|
|
66
|
+
* Requires a viax token (session must be ready). Runs once on mount.
|
|
67
|
+
*/
|
|
68
|
+
export function useIsSubscriberQuery(client, sessionReady) {
|
|
69
|
+
return useQuery({
|
|
70
|
+
queryKey: ['checkout', 'isCurrentUserSubscriber'],
|
|
71
|
+
enabled: !!client && sessionReady,
|
|
72
|
+
queryFn: () => client.isCurrentUserSubscriber(),
|
|
73
|
+
staleTime: Infinity,
|
|
74
|
+
retry: 1,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Polls the cart state machine every second; stops on terminal states. */
|
|
78
|
+
export function useCartStatusQuery(client, cartBiId) {
|
|
79
|
+
return useQuery({
|
|
80
|
+
queryKey: checkoutKeys.status(cartBiId !== null && cartBiId !== void 0 ? cartBiId : 'none'),
|
|
81
|
+
enabled: !!client && !!cartBiId,
|
|
82
|
+
queryFn: () => client.getCartStatus(cartBiId),
|
|
83
|
+
refetchInterval: (query) => { var _a, _b; return isTerminalStatus((_b = (_a = query.state.data) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : null) ? false : 1000; },
|
|
84
|
+
refetchOnWindowFocus: false,
|
|
85
|
+
gcTime: 0,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Looks up the created subscription biId after success. */
|
|
89
|
+
export function useSubscriptionIdQuery(client, cartBiId, enabled) {
|
|
90
|
+
return useQuery({
|
|
91
|
+
queryKey: checkoutKeys.subscriptionId(cartBiId !== null && cartBiId !== void 0 ? cartBiId : 'none'),
|
|
92
|
+
enabled: enabled && !!client && !!cartBiId,
|
|
93
|
+
queryFn: () => client.findSubscriptionBiId(cartBiId),
|
|
94
|
+
staleTime: Infinity,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fetches course details when the user arrived via ?course_run_key=.
|
|
99
|
+
* Skipped entirely when no course key is present (standard checkout).
|
|
100
|
+
*/
|
|
101
|
+
export function useCourseDetailQuery(client, sessionReady, courseRunKey) {
|
|
102
|
+
return useQuery({
|
|
103
|
+
queryKey: checkoutKeys.courseDetail(courseRunKey !== null && courseRunKey !== void 0 ? courseRunKey : 'none'),
|
|
104
|
+
enabled: !!client && sessionReady && !!courseRunKey,
|
|
105
|
+
queryFn: () => client.getCourseDetail(courseRunKey),
|
|
106
|
+
staleTime: Infinity,
|
|
107
|
+
retry: 1,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=useCheckoutQueries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCheckoutQueries.js","sourceRoot":"","sources":["../../../src/data/hooks/useCheckoutQueries.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * query/useCheckoutQueries.ts — read-side TanStack Query hooks.\n *\n * useViaxSessionQuery runs on mount to exchange the edX session for a viax\n * token. Billing plans and subscriber count wait until the session is ready.\n */\nimport { useQuery } from '@tanstack/react-query';\nimport { isTerminalStatus } from '../../constants';\nimport { checkoutKeys } from './keys';\nimport { fetchEdxSession, fetchUserCountry } from '../../utils/provision';\nimport type { CheckoutClient, CartStatusResult, BillingPlansResult, CourseDetail } from '../../types';\n\n/** Exchange edX session cookies for a viax token on page load. */\nexport function useViaxSessionQuery(sessionUrl: string | null) {\n return useQuery({\n queryKey: checkoutKeys.viaxSession(),\n enabled: !!sessionUrl,\n queryFn: () => fetchEdxSession(sessionUrl!),\n staleTime: 5 * 60 * 1000,\n retry: 1,\n });\n}\n\n/**\n * Fetches billing plans after the viax session is established.\n * Used to display prices in CartSummary before the user starts checkout.\n */\nexport function useBillingPlansQuery(client: CheckoutClient | null, sessionReady: boolean) {\n return useQuery({\n queryKey: checkoutKeys.billingPlans(),\n enabled: !!client && sessionReady,\n queryFn: (): Promise<BillingPlansResult> => client!.getBillingPlans(),\n staleTime: Infinity,\n retry: 2,\n });\n}\n\n/**\n * Fetches the live active subscriber count after the viax session is ready.\n * Refreshes every 5 minutes so the number stays roughly current.\n */\nexport function useActiveSubscriptionsQuery(client: CheckoutClient | null, sessionReady: boolean) {\n return useQuery({\n queryKey: ['checkout', 'activeSubscriptions'],\n enabled: !!client && sessionReady,\n queryFn: () => client!.getActiveSubscriptions(),\n staleTime: 5 * 60 * 1000, // 5 minutes\n retry: 1,\n });\n}\n\n/**\n * Fetches the user's country from their edX account profile.\n * Derives the LMS base URL from the session URL (same origin).\n * Returns null if the fetch fails or the field is absent — checkout stays open in that case.\n */\nexport function useUserCountryQuery(sessionUrl: string | null, username: string | null) {\n return useQuery({\n queryKey: ['checkout', 'userCountry', username],\n enabled: !!sessionUrl && !!username,\n queryFn: () => {\n const lmsBaseUrl = new URL(sessionUrl!).origin;\n return fetchUserCountry(lmsBaseUrl, username!);\n },\n staleTime: Infinity,\n retry: 1,\n });\n}\n\n/**\n * Checks whether the current user already has an active subscription.\n * Requires a viax token (session must be ready). Runs once on mount.\n */\nexport function useIsSubscriberQuery(client: CheckoutClient | null, sessionReady: boolean) {\n return useQuery({\n queryKey: ['checkout', 'isCurrentUserSubscriber'],\n enabled: !!client && sessionReady,\n queryFn: () => client!.isCurrentUserSubscriber(),\n staleTime: Infinity,\n retry: 1,\n });\n}\n\n/** Polls the cart state machine every second; stops on terminal states. */\nexport function useCartStatusQuery(client: CheckoutClient | null, cartBiId: string | null) {\n return useQuery({\n queryKey: checkoutKeys.status(cartBiId ?? 'none'),\n enabled: !!client && !!cartBiId,\n queryFn: (): Promise<CartStatusResult> => client!.getCartStatus(cartBiId!),\n refetchInterval: (query) =>\n isTerminalStatus(query.state.data?.status ?? null) ? false : 1000,\n refetchOnWindowFocus: false,\n gcTime: 0,\n });\n}\n\n/** Looks up the created subscription biId after success. */\nexport function useSubscriptionIdQuery(\n client: CheckoutClient | null,\n cartBiId: string | null,\n enabled: boolean,\n) {\n return useQuery({\n queryKey: checkoutKeys.subscriptionId(cartBiId ?? 'none'),\n enabled: enabled && !!client && !!cartBiId,\n queryFn: () => client!.findSubscriptionBiId(cartBiId!),\n staleTime: Infinity,\n });\n}\n\n/**\n * Fetches course details when the user arrived via ?course_run_key=.\n * Skipped entirely when no course key is present (standard checkout).\n */\nexport function useCourseDetailQuery(\n client: CheckoutClient | null,\n sessionReady: boolean,\n courseRunKey: string | null,\n) {\n return useQuery({\n queryKey: checkoutKeys.courseDetail(courseRunKey ?? 'none'),\n enabled: !!client && sessionReady && !!courseRunKey,\n queryFn: (): Promise<CourseDetail | null> => client!.getCourseDetail(courseRunKey!),\n staleTime: Infinity,\n retry: 1,\n });\n}\n"]}
|
package/dist/routes.d.ts
CHANGED
package/dist/routes.js
CHANGED
|
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { authenticatedLoader, getAuthenticatedHttpClient } from '@openedx/frontend-base';
|
|
10
|
+
import { authenticatedLoader, getAuthenticatedHttpClient, } from '@openedx/frontend-base';
|
|
11
11
|
import { redirect } from 'react-router-dom';
|
|
12
12
|
import { getSubsInitApiUrl } from './data/services/subs/urls';
|
|
13
13
|
import { subscriptionManageRole } from './constants';
|
|
@@ -36,15 +36,16 @@ const protectedLoader = (args) => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
36
36
|
});
|
|
37
37
|
const routes = [
|
|
38
38
|
{
|
|
39
|
-
id: 'org.openedx.frontend.route.
|
|
40
|
-
path: '
|
|
39
|
+
id: 'org.openedx.frontend.route.template.main',
|
|
40
|
+
path: 'checkout-page',
|
|
41
41
|
loader: protectedLoader,
|
|
42
42
|
handle: {
|
|
43
43
|
roles: [subscriptionManageRole],
|
|
44
44
|
},
|
|
45
45
|
lazy() {
|
|
46
46
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
-
const module = yield import(
|
|
47
|
+
const module = yield import(
|
|
48
|
+
/* webpackChunkName: "template-main" */ './Main');
|
|
48
49
|
return { Component: module.default };
|
|
49
50
|
});
|
|
50
51
|
},
|
|
@@ -53,7 +54,8 @@ const routes = [
|
|
|
53
54
|
index: true,
|
|
54
55
|
lazy() {
|
|
55
56
|
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
-
const module = yield import(
|
|
57
|
+
const module = yield import(
|
|
58
|
+
/* webpackChunkName: "CheckoutPage" */ './checkoutPage/CheckoutPage');
|
|
57
59
|
return { Component: module.default };
|
|
58
60
|
});
|
|
59
61
|
},
|
package/dist/routes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.tsx"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.tsx"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EACL,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,sDAAsD;AACtD,MAAM,wBAAwB,GAAG,GAAS,EAAE;IAC1C,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,GACV,MAAM,0BAA0B,EAAE,CAAC,GAAG,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAEhE,IAAI,CAAC,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,gBAAgB,CAAA,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,YAAY,EAAE,CAAC;YACnE,MAAM,QAAQ,CAAC,oBAAoB,CAAC,CAAC,CAAC,0EAA0E;QAClH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,mCAAmC;QACnC,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;YAC9B,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,QAAQ,CAAC,oBAAoB,CAAC,CAAC,CAAC,mDAAmD;IAC3F,CAAC;AACH,CAAC,CAAA,CAAC;AAEF,yEAAyE;AACzE,MAAM,eAAe,GAAG,CAAO,IAAI,EAAE,EAAE;IACrC,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,wBAAwB,EAAE,CAAC;IAEjC,OAAO,IAAI,CAAC;AACd,CAAC,CAAA,CAAC;AAEF,MAAM,MAAM,GAAG;IACb;QACE,EAAE,EAAE,0CAA0C;QAC9C,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,eAAe;QACvB,MAAM,EAAE;YACN,KAAK,EAAE,CAAC,sBAAsB,CAAC;SAChC;QACK,IAAI;;gBACR,MAAM,MAAM,GAAG,MAAM,MAAM;gBACzB,uCAAuC,CAAC,QAAQ,CACjD,CAAC;gBACF,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;YACvC,CAAC;SAAA;QACD,QAAQ,EAAE;YACR;gBACE,KAAK,EAAE,IAAI;gBACL,IAAI;;wBACR,MAAM,MAAM,GAAG,MAAM,MAAM;wBACzB,sCAAsC,CAAC,6BAA6B,CACrE,CAAC;wBACF,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;oBACvC,CAAC;iBAAA;aACF;SACF;KACF;CACF,CAAC;AAEF,eAAe,MAAM,CAAC","sourcesContent":["import {\n authenticatedLoader,\n getAuthenticatedHttpClient,\n} from '@openedx/frontend-base';\nimport { redirect } from 'react-router-dom';\n\nimport { getSubsInitApiUrl } from './data/services/subs/urls';\nimport { subscriptionManageRole } from './constants';\n\n// loader to check if the user has subscription access\nconst subscriptionAccessLoader = async () => {\n try {\n const { data }\n = await getAuthenticatedHttpClient().get(getSubsInitApiUrl());\n\n if (!data?.userSubscription || !data.userSubscription.isSubscriber) {\n throw redirect('/learner-dashboard'); // redirect to learner dashboard if user does not have subscription access\n }\n\n return null;\n } catch (error) {\n // Let redirect responses bubble up\n if (error instanceof Response) {\n throw error;\n }\n\n throw redirect('/learner-dashboard'); // redirect to learner dashboard if an error occurs\n }\n};\n\n// composed loader that checks for authentication and subscription access\nconst protectedLoader = async (args) => {\n await authenticatedLoader(args);\n await subscriptionAccessLoader();\n\n return null;\n};\n\nconst routes = [\n {\n id: 'org.openedx.frontend.route.template.main',\n path: 'checkout-page',\n loader: protectedLoader,\n handle: {\n roles: [subscriptionManageRole],\n },\n async lazy() {\n const module = await import(\n /* webpackChunkName: \"template-main\" */ './Main'\n );\n return { Component: module.default };\n },\n children: [\n {\n index: true,\n async lazy() {\n const module = await import(\n /* webpackChunkName: \"CheckoutPage\" */ './checkoutPage/CheckoutPage'\n );\n return { Component: module.default };\n },\n },\n ],\n },\n];\n\nexport default routes;\n"]}
|
package/dist/style.scss
CHANGED
|
@@ -1 +1,14 @@
|
|
|
1
1
|
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints.css";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
//components
|
|
5
|
+
@use "./styles/components/checkout.scss";
|
|
6
|
+
@use "./styles/components/enrollmentSummary.scss";
|
|
7
|
+
@use "./styles/components/paymentForm.scss";
|
|
8
|
+
@use "./styles/components/checkoutStepper.scss";
|
|
9
|
+
@use "./styles/components/paymentFields.scss";
|
|
10
|
+
@use "./styles/components/brandMarks.scss";
|
|
11
|
+
@use "./styles/components/statusScreens.scss";
|
|
12
|
+
@use "./styles/components/edxUnlimitedBanner.scss";
|
|
13
|
+
@use "./styles/components/cartSummary.scss";
|
|
14
|
+
@use "./styles/components/courseEnrollmentCard.scss";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.brand-marks {
|
|
2
|
+
&__card-brands {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
flex-shrink: 0;
|
|
6
|
+
gap: 4px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
&__paypal {
|
|
10
|
+
font-family: Arial, sans-serif;
|
|
11
|
+
font-weight: 700;
|
|
12
|
+
font-style: italic;
|
|
13
|
+
font-size: 1.05rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&__paypal-pay {
|
|
17
|
+
color: #003087;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&__paypal-pal {
|
|
21
|
+
color: #009cde;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&__edx-logo {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
flex-shrink: 0;
|
|
29
|
+
width: 56px;
|
|
30
|
+
height: 40px;
|
|
31
|
+
border-radius: 6px;
|
|
32
|
+
background: #1f1f1f;
|
|
33
|
+
line-height: 1;
|
|
34
|
+
margin-right: 0.75rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&__edx-logo-text {
|
|
38
|
+
text-align: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&__edx-logo-edx {
|
|
42
|
+
color: #fff;
|
|
43
|
+
font-weight: 800;
|
|
44
|
+
font-size: 0.8rem;
|
|
45
|
+
letter-spacing: -0.02em;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&__edx-logo-unlimited {
|
|
49
|
+
color: #b1c2ff;
|
|
50
|
+
font-size: 0.5rem;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
margin-top: 1px;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// styles/components/cartSummary.scss
|
|
2
|
+
|
|
3
|
+
.cart-summary {
|
|
4
|
+
// No background — inherits the page background (off-white in the design)
|
|
5
|
+
background: transparent;
|
|
6
|
+
|
|
7
|
+
&__stats {
|
|
8
|
+
display: flex;
|
|
9
|
+
gap: 1.5rem;
|
|
10
|
+
margin-top: 1rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&__stat {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 0.5rem;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
div.checkout__container {
|
|
2
|
+
padding-inline: 1.125rem; /* 18px */
|
|
3
|
+
padding-block: 3rem; /* 48px */
|
|
4
|
+
div.payment__options {
|
|
5
|
+
> div {
|
|
6
|
+
padding: 32px;
|
|
7
|
+
background-color: white;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media (--pgn-size-breakpoint-min-width-lg) {
|
|
13
|
+
div.checkout__container {
|
|
14
|
+
padding:0;
|
|
15
|
+
div.payment__options {
|
|
16
|
+
background-color: var(--pgn-color-light-100);
|
|
17
|
+
> div {
|
|
18
|
+
padding: 32px 32px 64px 32px;
|
|
19
|
+
background-color: transparent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
div.payment__summary {
|
|
23
|
+
padding: 48px 48px 32px 48px;
|
|
24
|
+
}
|
|
25
|
+
div.gap-5 {
|
|
26
|
+
gap: 0 !important;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.course-enrollment-card {
|
|
2
|
+
margin-top: 1.25rem;
|
|
3
|
+
padding: 1rem;
|
|
4
|
+
background: var(--pgn-color-white, #fff);
|
|
5
|
+
border: 1px solid var(--pgn-color-light-400, #e5e5e5);
|
|
6
|
+
border-radius: var(--pgn-size-border-radius-base, 0.375rem);
|
|
7
|
+
|
|
8
|
+
&__image {
|
|
9
|
+
display: block;
|
|
10
|
+
width: 4.5rem;
|
|
11
|
+
height: 3.25rem;
|
|
12
|
+
object-fit: cover;
|
|
13
|
+
border-radius: 0.25rem;
|
|
14
|
+
background: var(--pgn-color-light-300, #f2f2f2);
|
|
15
|
+
|
|
16
|
+
&--placeholder {
|
|
17
|
+
background:
|
|
18
|
+
linear-gradient(
|
|
19
|
+
to top right,
|
|
20
|
+
transparent calc(50% - 1px),
|
|
21
|
+
var(--pgn-color-light-500, #c2c2c2) 50%,
|
|
22
|
+
transparent calc(50% + 1px)
|
|
23
|
+
),
|
|
24
|
+
linear-gradient(
|
|
25
|
+
to top left,
|
|
26
|
+
transparent calc(50% - 1px),
|
|
27
|
+
var(--pgn-color-light-500, #c2c2c2) 50%,
|
|
28
|
+
transparent calc(50% + 1px)
|
|
29
|
+
),
|
|
30
|
+
var(--pgn-color-light-300, #f2f2f2);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&__copy {
|
|
35
|
+
color: var(--pgn-color-gray-900, #212529);
|
|
36
|
+
line-height: 1.4;
|
|
37
|
+
}
|
|
38
|
+
}
|