@osimart/create-storefront 0.0.0-stage → 1.0.1
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/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.js +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.js +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +282 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +44 -4
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
<!-- components/checkout/PaymentHandoff.vue -->
|
|
2
|
+
<script setup>
|
|
3
|
+
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from "vue";
|
|
4
|
+
import paymentServices from "#osi/services/PaymentServices";
|
|
5
|
+
|
|
6
|
+
const MPGS_SCRIPT_ID = "mc-checkout-script";
|
|
7
|
+
const MPGS_TARGET_ID = "mc-payment-content";
|
|
8
|
+
|
|
9
|
+
// Unified Checkout needs TWO containers. If you only give it paymentSelection
|
|
10
|
+
// it renders the button list inline and puts the card form in its own sidebar
|
|
11
|
+
// overlay - which is what was happening. Supplying paymentScreen keeps every
|
|
12
|
+
// screen inside this modal.
|
|
13
|
+
const UC_SELECTION_ID = "uc-payment-selection";
|
|
14
|
+
const UC_SCREEN_ID = "uc-payment-screen";
|
|
15
|
+
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
payment: { type: Object, required: true },
|
|
18
|
+
// Optional: shown in the summary strip at the top of the modal.
|
|
19
|
+
amount: { type: [String, Number], default: null },
|
|
20
|
+
currency: { type: String, default: "USD" },
|
|
21
|
+
title: { type: String, default: "Payment" },
|
|
22
|
+
// MPGS ships two Hosted Checkout builds against the same session. The
|
|
23
|
+
// versioned one (/checkout/version/NN/checkout.js) renders a summary with a
|
|
24
|
+
// "Checkout" button that opens a side panel; the legacy one
|
|
25
|
+
// (/static/checkout/checkout.min.js) renders the payment tiles inline, which
|
|
26
|
+
// is what we want inside this modal. Set false to load the backend URL
|
|
27
|
+
// verbatim.
|
|
28
|
+
inlineMpgs: { type: Boolean, default: true },
|
|
29
|
+
});
|
|
30
|
+
const emit = defineEmits(["error", "settled", "declined", "close"]);
|
|
31
|
+
|
|
32
|
+
// The amount strip shows the store's currency symbol, not a hardcoded "$".
|
|
33
|
+
const { store } = useStorefront();
|
|
34
|
+
const currencySymbol = computed(
|
|
35
|
+
() => store.value?.default_currency?.symbol || store.value?.default_currency?.code || "$"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const error = ref("");
|
|
39
|
+
// A decline is not an error: nothing broke, the bank said no. It gets its own
|
|
40
|
+
// state so the modal can offer a retry instead of a dead "Close" button.
|
|
41
|
+
const declined = ref("");
|
|
42
|
+
const loading = ref(props.payment.flow === "CLIENT_SECRET");
|
|
43
|
+
|
|
44
|
+
const isEmbedded = computed(
|
|
45
|
+
() =>
|
|
46
|
+
props.payment.flow === "HOSTED_SESSION" ||
|
|
47
|
+
props.payment.flow === "CLIENT_SECRET",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const subtitle = computed(() => {
|
|
51
|
+
const p = props.payment;
|
|
52
|
+
if (p.merchant_name) return p.merchant_name;
|
|
53
|
+
if (p.reference) return `Ref ${p.reference}`;
|
|
54
|
+
return "Complete your payment securely";
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Keep the gateway host the backend sent, but swap a versioned Hosted Checkout
|
|
58
|
+
// path for the legacy build so showEmbeddedPage() renders the method tiles in
|
|
59
|
+
// the container rather than a button that opens a side panel.
|
|
60
|
+
function mpgsScriptUrl(url) {
|
|
61
|
+
if (!props.inlineMpgs) return url;
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(url, window.location.origin);
|
|
64
|
+
if (!/\/checkout\/version\/\d+\/checkout(\.min)?\.js$/.test(u.pathname)) {
|
|
65
|
+
return url;
|
|
66
|
+
}
|
|
67
|
+
u.pathname = "/static/checkout/checkout.min.js";
|
|
68
|
+
u.search = "";
|
|
69
|
+
return u.toString();
|
|
70
|
+
} catch {
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Appended with an error path, unlike Osina - a blocked CDN there just leaves
|
|
76
|
+
// an empty box.
|
|
77
|
+
function loadMpgsScript(src) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const existing = document.getElementById(MPGS_SCRIPT_ID);
|
|
80
|
+
if (existing) {
|
|
81
|
+
// A tag being present does not mean it finished loading - a second
|
|
82
|
+
// attempt while the first is still in flight would throw on
|
|
83
|
+
// window.Checkout.
|
|
84
|
+
if (window.Checkout) return resolve();
|
|
85
|
+
existing.addEventListener("load", () => resolve(), { once: true });
|
|
86
|
+
existing.addEventListener(
|
|
87
|
+
"error",
|
|
88
|
+
() => reject(new Error("Could not load the payment library.")),
|
|
89
|
+
{ once: true },
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const script = document.createElement("script");
|
|
94
|
+
script.id = MPGS_SCRIPT_ID;
|
|
95
|
+
script.src = src;
|
|
96
|
+
script.onload = resolve;
|
|
97
|
+
script.onerror = () =>
|
|
98
|
+
reject(new Error("Could not load the payment library."));
|
|
99
|
+
document.body.appendChild(script);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loadScript(src, { globalKey, ...attrs } = {}) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const existing = document.querySelector(`script[src="${src}"]`);
|
|
106
|
+
if (existing && globalKey && window[globalKey]) return resolve();
|
|
107
|
+
if (existing) {
|
|
108
|
+
existing.addEventListener("load", () => resolve(), { once: true });
|
|
109
|
+
existing.addEventListener(
|
|
110
|
+
"error",
|
|
111
|
+
() => reject(new Error("Could not load the payment library.")),
|
|
112
|
+
{ once: true },
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const el = Object.assign(document.createElement("script"), {
|
|
117
|
+
src,
|
|
118
|
+
async: true,
|
|
119
|
+
...attrs,
|
|
120
|
+
});
|
|
121
|
+
el.onload = resolve;
|
|
122
|
+
el.onerror = () => reject(new Error("Could not load the payment library."));
|
|
123
|
+
document.head.appendChild(el);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The capture context (client_secret) is itself a JWT. Its payload carries
|
|
128
|
+
// the exact library URL + integrity hash to load for this client_version -
|
|
129
|
+
// Cybersource docs say to always read these from the token rather than
|
|
130
|
+
// hardcoding a URL, since the asset path is versioned per release
|
|
131
|
+
// (e.g. /uc/v1/assets/0.24.2/SecureAcceptance.js).
|
|
132
|
+
function decodeCaptureContext(jwt) {
|
|
133
|
+
const payloadSegment = jwt.split(".")[1];
|
|
134
|
+
const base64 = payloadSegment.replace(/-/g, "+").replace(/_/g, "/");
|
|
135
|
+
const json = decodeURIComponent(
|
|
136
|
+
atob(base64)
|
|
137
|
+
.split("")
|
|
138
|
+
.map((c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0"))
|
|
139
|
+
.join(""),
|
|
140
|
+
);
|
|
141
|
+
const payload = JSON.parse(json);
|
|
142
|
+
return payload?.ctx?.[0]?.data || {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fail(message) {
|
|
146
|
+
error.value = message;
|
|
147
|
+
loading.value = false;
|
|
148
|
+
emit("error", message);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The bank refused, or Cybersource refused on the bank's behalf. The order
|
|
152
|
+
// exists and is unpaid, which is correct - the buyer can try again.
|
|
153
|
+
function decline(reason) {
|
|
154
|
+
declined.value = reason || "Your payment was declined. Please try another card.";
|
|
155
|
+
loading.value = false;
|
|
156
|
+
emit("declined", declined.value);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onKeydown(e) {
|
|
160
|
+
if (e.key === "Escape") emit("close");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let previousOverflow = "";
|
|
164
|
+
|
|
165
|
+
onMounted(async () => {
|
|
166
|
+
previousOverflow = document.body.style.overflow;
|
|
167
|
+
document.body.style.overflow = "hidden";
|
|
168
|
+
window.addEventListener("keydown", onKeydown);
|
|
169
|
+
|
|
170
|
+
const p = props.payment;
|
|
171
|
+
try {
|
|
172
|
+
// Switch on flow, never on provider. Adding a gateway stays a backend change.
|
|
173
|
+
if (p.flow === "HOSTED_SESSION") {
|
|
174
|
+
if (!p.checkout_js_url || !p.session_id) {
|
|
175
|
+
return fail("This payment could not be started. Please try again.");
|
|
176
|
+
}
|
|
177
|
+
await loadMpgsScript(mpgsScriptUrl(p.checkout_js_url));
|
|
178
|
+
await nextTick(); // #mc-payment-content must exist before configure()
|
|
179
|
+
if (!window.Checkout) {
|
|
180
|
+
return fail("The payment library did not load. Please try again.");
|
|
181
|
+
}
|
|
182
|
+
// No interaction block by default, like Osina: whatever the backend set
|
|
183
|
+
// on CREATE_CHECKOUT_SESSION is what renders. Hardcoding displayControl
|
|
184
|
+
// here is what was hiding the billing address section.
|
|
185
|
+
window.Checkout.configure({
|
|
186
|
+
session: { id: p.session_id },
|
|
187
|
+
...(p.display_control
|
|
188
|
+
? { interaction: { displayControl: p.display_control } }
|
|
189
|
+
: {}),
|
|
190
|
+
});
|
|
191
|
+
window.Checkout.showEmbeddedPage(`#${MPGS_TARGET_ID}`);
|
|
192
|
+
} else if (p.flow === "REDIRECT") {
|
|
193
|
+
if (!p.redirect_url) {
|
|
194
|
+
return fail("This payment could not be started. Please try again.");
|
|
195
|
+
}
|
|
196
|
+
window.location.href = p.redirect_url;
|
|
197
|
+
} else if (p.flow === "CLIENT_SECRET") {
|
|
198
|
+
// Cybersource Unified Checkout. client_secret is the capture context
|
|
199
|
+
// JWT - decode it for the versioned library URL + integrity hash.
|
|
200
|
+
if (!p.client_secret) {
|
|
201
|
+
return fail("This payment could not be started. Please try again.");
|
|
202
|
+
}
|
|
203
|
+
const ctxData = decodeCaptureContext(p.client_secret);
|
|
204
|
+
if (!ctxData.clientLibrary) {
|
|
205
|
+
return fail("This payment could not be started. Please try again.");
|
|
206
|
+
}
|
|
207
|
+
await loadScript(ctxData.clientLibrary, {
|
|
208
|
+
globalKey: "Accept",
|
|
209
|
+
integrity: ctxData.clientLibraryIntegrity,
|
|
210
|
+
crossOrigin: "anonymous",
|
|
211
|
+
});
|
|
212
|
+
await nextTick();
|
|
213
|
+
if (!window.Accept) {
|
|
214
|
+
return fail("The payment library did not load. Please try again.");
|
|
215
|
+
}
|
|
216
|
+
const accept = await window.Accept(p.client_secret);
|
|
217
|
+
// sidebar = false. Combined with a paymentScreen container below, every
|
|
218
|
+
// Unified Checkout screen renders inside this modal instead of the
|
|
219
|
+
// gateway's own right-hand overlay.
|
|
220
|
+
const unifiedPayments = await accept.unifiedPayments(false);
|
|
221
|
+
loading.value = false;
|
|
222
|
+
const transientToken = await unifiedPayments.show({
|
|
223
|
+
containers: {
|
|
224
|
+
paymentSelection: `#${UC_SELECTION_ID}`,
|
|
225
|
+
paymentScreen: `#${UC_SCREEN_ID}`,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
// The library only tokenizes the card - the backend still has to submit
|
|
229
|
+
// transientToken to Cybersource's Payments API to actually authorize the
|
|
230
|
+
// charge before this order can be considered paid.
|
|
231
|
+
//
|
|
232
|
+
// This call returns HTTP 200 for a DECLINE as well as for a success -
|
|
233
|
+
// the outcome is in the body, not the status code. Awaiting it without
|
|
234
|
+
// reading `paid` marks an unpaid order complete, which is what an
|
|
235
|
+
// insufficient-funds card was doing.
|
|
236
|
+
const result = await paymentServices.completeClientSecretPayment(
|
|
237
|
+
p.reference,
|
|
238
|
+
transientToken,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (!result?.paid) {
|
|
242
|
+
return decline(result?.reason);
|
|
243
|
+
}
|
|
244
|
+
emit("settled", { ...p, ...result });
|
|
245
|
+
} else if (p.flow === "OFFLINE") {
|
|
246
|
+
emit("settled", p); // already placed, nothing to hand off
|
|
247
|
+
} else {
|
|
248
|
+
fail("This payment method is not supported.");
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
fail(e?.data?.detail || e?.message || "We could not start this payment.");
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
onBeforeUnmount(() => {
|
|
256
|
+
window.removeEventListener("keydown", onKeydown);
|
|
257
|
+
document.body.style.overflow = previousOverflow;
|
|
258
|
+
});
|
|
259
|
+
</script>
|
|
260
|
+
|
|
261
|
+
<template>
|
|
262
|
+
<Teleport defer to="body">
|
|
263
|
+
<div
|
|
264
|
+
class="fixed inset-0 z-[100] font-body flex items-center justify-center bg-black bg-opacity-50 px-4"
|
|
265
|
+
@click.self="emit('close')"
|
|
266
|
+
>
|
|
267
|
+
<div
|
|
268
|
+
class="bg-white rounded-xl shadow-2xl w-full max-w-xl max-h-[92vh] overflow-y-auto"
|
|
269
|
+
>
|
|
270
|
+
<!-- header -->
|
|
271
|
+
<div
|
|
272
|
+
class="flex items-center justify-start gap-3 px-5 sm:px-6 py-4 border-b border-gray-200"
|
|
273
|
+
>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
class="text-gray-500 rounded-full bg-gray-100 p-2 hover:text-primaryColor shrink-0 ml-3"
|
|
277
|
+
@click="emit('close')"
|
|
278
|
+
>
|
|
279
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 72 72" id="emoji">
|
|
280
|
+
<g id="color"/>
|
|
281
|
+
<g id="hair"/>
|
|
282
|
+
<g id="skin"/>
|
|
283
|
+
<g id="skin-shadow"/>
|
|
284
|
+
<g id="line">
|
|
285
|
+
<polyline fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="46.1964,16.2048 26.8036,35.6651 46.1964,55.1254"/>
|
|
286
|
+
</g>
|
|
287
|
+
</svg>
|
|
288
|
+
</button>
|
|
289
|
+
<div class="min-w-0">
|
|
290
|
+
<h2 class="text-lg font-semibold text-primaryColor">{{ title }}</h2>
|
|
291
|
+
<p class="text-sm text-gray-500 mt-0.5 truncate">{{ subtitle }}</p>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<!-- amount strip -->
|
|
297
|
+
<div
|
|
298
|
+
v-if="amount !== null && amount !== ''"
|
|
299
|
+
class="mx-5 sm:mx-6 mt-4 bg-gray-50 rounded-lg px-4 py-3 flex justify-between items-center gap-3"
|
|
300
|
+
>
|
|
301
|
+
<div class="min-w-0">
|
|
302
|
+
<p class="text-sm font-semibold text-primaryColor">Amount due</p>
|
|
303
|
+
<p class="text-xs text-gray-500">Paid securely by card</p>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="text-right shrink-0">
|
|
306
|
+
<p class="text-lg font-semibold text-primaryColor">{{ currencySymbol }}{{ amount }}</p>
|
|
307
|
+
<p class="text-xs text-gray-500">{{ currency }}</p>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<!-- body -->
|
|
312
|
+
<div class="px-5 sm:px-6 pt-4 pb-6">
|
|
313
|
+
<p
|
|
314
|
+
v-if="error"
|
|
315
|
+
class="text-sm text-red-600 bg-red-50 rounded-lg px-3 py-2 text-center"
|
|
316
|
+
>
|
|
317
|
+
{{ error }}
|
|
318
|
+
</p>
|
|
319
|
+
|
|
320
|
+
<!-- A decline ends this attempt: the transient token is single-use
|
|
321
|
+
and now spent, so retrying means a fresh checkout, not a
|
|
322
|
+
resubmit of the same form. -->
|
|
323
|
+
<div v-else-if="declined" class="py-2">
|
|
324
|
+
<p
|
|
325
|
+
class="text-sm text-red-600 bg-red-50 rounded-lg px-3 py-2 text-center"
|
|
326
|
+
>
|
|
327
|
+
{{ declined }}
|
|
328
|
+
</p>
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
class="osi-btn osi-btn--block mt-4"
|
|
332
|
+
@click="emit('close')"
|
|
333
|
+
>
|
|
334
|
+
Try another card
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<p
|
|
339
|
+
v-else-if="loading"
|
|
340
|
+
class="text-sm text-gray-500 text-center py-6"
|
|
341
|
+
>
|
|
342
|
+
Loading secure payment…
|
|
343
|
+
</p>
|
|
344
|
+
|
|
345
|
+
<p
|
|
346
|
+
v-if="payment.flow === 'REDIRECT' && !error && !declined"
|
|
347
|
+
class="text-sm text-gray-500 text-center py-6"
|
|
348
|
+
>
|
|
349
|
+
Taking you to the payment page…
|
|
350
|
+
</p>
|
|
351
|
+
|
|
352
|
+
<!-- v-show, not v-if: the nodes must be in the DOM before
|
|
353
|
+
configure()/show() is called -->
|
|
354
|
+
<div
|
|
355
|
+
v-show="payment.flow === 'HOSTED_SESSION' && !error && !declined"
|
|
356
|
+
:id="MPGS_TARGET_ID"
|
|
357
|
+
class="h-fit"
|
|
358
|
+
/>
|
|
359
|
+
|
|
360
|
+
<!-- Unified Checkout: button list on top, card form directly under
|
|
361
|
+
it. The list is not optional in the v0 library - the form only
|
|
362
|
+
mounts after a payment type is picked - but with paymentScreen
|
|
363
|
+
supplied it opens in place instead of the sidebar. -->
|
|
364
|
+
<div v-show="payment.flow === 'CLIENT_SECRET' && !error && !declined">
|
|
365
|
+
<div :id="UC_SELECTION_ID" />
|
|
366
|
+
<div :id="UC_SCREEN_ID" class="mt-3 h-fit" />
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<button
|
|
370
|
+
v-if="error"
|
|
371
|
+
type="button"
|
|
372
|
+
class="osi-btn osi-btn--block mt-4"
|
|
373
|
+
@click="emit('close')"
|
|
374
|
+
>
|
|
375
|
+
Close
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div
|
|
380
|
+
class="flex items-center justify-center gap-2 px-5 sm:px-6 border-t border-gray-100"
|
|
381
|
+
>
|
|
382
|
+
<img src="https://www.netcommercepay.com/new_logo/NCseal_S.png" border="0" alt="NetCommerce Security Seal">
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</Teleport>
|
|
388
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!---->
|
|
3
|
+
<div class="w-full h-[250px] sm:h-[40vh] relative bg-primaryColor">
|
|
4
|
+
<div class="w-full h-full absolute inset-0 flex justify-center items-center z-10 pointer-events-none">
|
|
5
|
+
<p class="text-secondaryColor font-heading uppercase text-2xl s:text-4xl text-center">
|
|
6
|
+
{{pageTitle}}
|
|
7
|
+
</p>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
import { Skeletor } from "vue-skeletor";
|
|
14
|
+
import "vue-skeletor/dist/vue-skeletor.css";
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
props: {
|
|
18
|
+
pageTitle: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: "",
|
|
21
|
+
},
|
|
22
|
+
isLoading: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
components: {
|
|
28
|
+
Skeletor,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
|
|
35
|
+
</style>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="isVisible"
|
|
3
|
+
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50"
|
|
4
|
+
@click.self="cancelDelete"
|
|
5
|
+
>
|
|
6
|
+
|
|
7
|
+
<div class="overflow-y animate-fade-in-up">
|
|
8
|
+
<div class="flex justify-center p-4 text-center items-center sm:p-0">
|
|
9
|
+
|
|
10
|
+
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:w-full sm:max-w-lg">
|
|
11
|
+
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
12
|
+
<div class="sm:flex sm:items-start">
|
|
13
|
+
<div class="mx-auto flex size-12 shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:size-10">
|
|
14
|
+
<svg class="size-6 text-primaryColor" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
|
15
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
|
16
|
+
</svg>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
19
|
+
<h3 class="text-base font-semibold text-gray-900" id="modal-title">Delete Address</h3>
|
|
20
|
+
<div class="mt-2">
|
|
21
|
+
<p class="text-sm text-gray-500">Are you sure you want to delete this address? This action cannot be undone.</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="bg-gray-50 px-4 py-3 flex flex-col gap-3 sm:flex-row-reverse sm:px-6">
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
class="osi-btn osi-btn--solid osi-btn--danger osi-btn--sm osi-confirm-btn"
|
|
30
|
+
:disabled="isLoading"
|
|
31
|
+
:aria-busy="isLoading"
|
|
32
|
+
@click="confirmDelete"
|
|
33
|
+
>
|
|
34
|
+
<span v-if="isLoading" class="osi-btn-spinner" aria-hidden="true"></span>
|
|
35
|
+
<span>Delete</span>
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
class="osi-btn osi-btn--secondary osi-btn--sm osi-confirm-btn"
|
|
40
|
+
:disabled="isLoading"
|
|
41
|
+
@click="cancelDelete"
|
|
42
|
+
>
|
|
43
|
+
Cancel
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
export default {
|
|
54
|
+
props: {
|
|
55
|
+
isVisible: Boolean,
|
|
56
|
+
jobId: String,
|
|
57
|
+
isLoading: {
|
|
58
|
+
type: Boolean,
|
|
59
|
+
default: false
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
mounted(){
|
|
63
|
+
document.body.classList.add('overflow-hidden');
|
|
64
|
+
|
|
65
|
+
},
|
|
66
|
+
methods: {
|
|
67
|
+
|
|
68
|
+
confirmDelete() {
|
|
69
|
+
this.$emit('confirm-delete', this.jobId);
|
|
70
|
+
document.body.classList.remove('overflow-hidden');
|
|
71
|
+
|
|
72
|
+
},
|
|
73
|
+
cancelDelete() {
|
|
74
|
+
this.$emit('cancel-delete');
|
|
75
|
+
document.body.classList.remove('overflow-hidden');
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style scoped>
|
|
83
|
+
@media (max-width: 639px) {
|
|
84
|
+
.osi-confirm-btn {
|
|
85
|
+
width: 100%;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@keyframes fadeInUp {
|
|
90
|
+
from {
|
|
91
|
+
opacity: 0;
|
|
92
|
+
transform: translate3d(0, 20px, 0);
|
|
93
|
+
}
|
|
94
|
+
to {
|
|
95
|
+
opacity: 1;
|
|
96
|
+
transform: translate3d(0, 0, 0);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.animate-fade-in-up {
|
|
101
|
+
animation: fadeInUp 0.3s ease-out forwards;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@keyframes spin {
|
|
105
|
+
from {
|
|
106
|
+
transform: rotate(0deg);
|
|
107
|
+
}
|
|
108
|
+
to {
|
|
109
|
+
transform: rotate(360deg);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.animate-spin {
|
|
114
|
+
animation: spin 1s linear infinite;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
117
|
+
|