@osimart/create-storefront 0.0.0-stage → 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 (206) hide show
  1. package/README.md +80 -2
  2. package/THEME_DEV_API.md +80 -0
  3. package/app/app.vue +154 -0
  4. package/app/assets/css/osi-controls.css +824 -0
  5. package/app/components/SectionRenderer.vue +46 -0
  6. package/app/components/builder/BuilderOverlay.vue +582 -0
  7. package/app/components/checkout/ExtraFees.vue +329 -0
  8. package/app/components/checkout/PaymentHandoff.vue +388 -0
  9. package/app/components/common/Banner.vue +35 -0
  10. package/app/components/common/ConfirmDeleteModal.vue +117 -0
  11. package/app/components/common/DropdownSelect.vue +432 -0
  12. package/app/components/common/Loader.vue +32 -0
  13. package/app/components/common/OsiField.vue +218 -0
  14. package/app/components/common/PhoneNumInput.vue +434 -0
  15. package/app/composables/UseBodyScrollLock.js +122 -0
  16. package/app/composables/UseThemeGroup.js +575 -0
  17. package/app/composables/useAddToCart.js +87 -0
  18. package/app/composables/useAnalytics.js +15 -0
  19. package/app/composables/useAnnouncements.js +29 -0
  20. package/app/composables/useApiErrorToast.js +104 -0
  21. package/app/composables/useAuth.js +99 -0
  22. package/app/composables/useBanners.js +25 -0
  23. package/app/composables/useBuilderPreview.js +320 -0
  24. package/app/composables/useButtonStyle.js +107 -0
  25. package/app/composables/useCarousel.js +160 -0
  26. package/app/composables/useCart.js +109 -0
  27. package/app/composables/useCheckout.js +76 -0
  28. package/app/composables/useClassificationItems.js +269 -0
  29. package/app/composables/useClassificationPage.js +545 -0
  30. package/app/composables/useClassificationUrl.js +63 -0
  31. package/app/composables/useColorScheme.js +44 -0
  32. package/app/composables/useContactForm.js +53 -0
  33. package/app/composables/useContentTree.js +770 -0
  34. package/app/composables/useDragScroll.js +73 -0
  35. package/app/composables/useFooterSocials.js +107 -0
  36. package/app/composables/useInputStyle.js +45 -0
  37. package/app/composables/useLayoutShell.js +114 -0
  38. package/app/composables/useLazyVideo.js +36 -0
  39. package/app/composables/useMediaTypes.js +66 -0
  40. package/app/composables/useMediaUrl.js +71 -0
  41. package/app/composables/useMenu.js +100 -0
  42. package/app/composables/useNavStyle.js +219 -0
  43. package/app/composables/usePlaceholders.js +294 -0
  44. package/app/composables/usePolicies.js +26 -0
  45. package/app/composables/useProductCardMedia.js +135 -0
  46. package/app/composables/useProductCardStyle.js +132 -0
  47. package/app/composables/useProductDetail.js +982 -0
  48. package/app/composables/useProductGallery.js +166 -0
  49. package/app/composables/useProductItems.js +270 -0
  50. package/app/composables/useProductUrl.js +60 -0
  51. package/app/composables/useProductVariants.js +210 -0
  52. package/app/composables/useQuickShop.js +115 -0
  53. package/app/composables/useRelatedProducts.js +186 -0
  54. package/app/composables/useRenderContext.js +45 -0
  55. package/app/composables/useScopedCss.js +106 -0
  56. package/app/composables/useSearch.js +35 -0
  57. package/app/composables/useSectionData.js +462 -0
  58. package/app/composables/useSectionShell.js +113 -0
  59. package/app/composables/useSidebar.js +69 -0
  60. package/app/composables/useStoreData.js +57 -0
  61. package/app/composables/useStoreInit.js +52 -0
  62. package/app/composables/useStorefront.js +239 -0
  63. package/app/composables/useT.js +21 -0
  64. package/app/composables/useTextStyles.js +84 -0
  65. package/app/composables/useThemeControls.js +34 -0
  66. package/app/composables/useThemeSeo.js +91 -0
  67. package/app/composables/useThemeTokens.js +171 -0
  68. package/app/composables/useWishlist.js +66 -0
  69. package/app/logic/components/AnnouncementBar.js +118 -0
  70. package/app/logic/components/CardGrid.js +125 -0
  71. package/app/logic/components/CardList.js +98 -0
  72. package/app/logic/components/CartDrawer.js +273 -0
  73. package/app/logic/components/CartItem.js +31 -0
  74. package/app/logic/components/ClassificationBento.js +6 -0
  75. package/app/logic/components/ClassificationCard.js +6 -0
  76. package/app/logic/components/ClassificationCarousel.js +115 -0
  77. package/app/logic/components/ClassificationEditorial.js +41 -0
  78. package/app/logic/components/ClassificationFeatured.js +57 -0
  79. package/app/logic/components/ClassificationGrid.js +6 -0
  80. package/app/logic/components/ClassificationNav.js +61 -0
  81. package/app/logic/components/ClassificationPart.js +72 -0
  82. package/app/logic/components/FooterBlock.js +91 -0
  83. package/app/logic/components/FooterSocials.js +60 -0
  84. package/app/logic/components/HeaderDropdownList.js +29 -0
  85. package/app/logic/components/HeaderIcon.js +68 -0
  86. package/app/logic/components/HeaderLogo.js +39 -0
  87. package/app/logic/components/HeaderNav.js +268 -0
  88. package/app/logic/components/MobileMenu.js +280 -0
  89. package/app/logic/components/ProductBuyButtons.js +66 -0
  90. package/app/logic/components/ProductCard.js +160 -0
  91. package/app/logic/components/ProductFilters.js +37 -0
  92. package/app/logic/components/ProductGallery.js +19 -0
  93. package/app/logic/components/ProductInfoBlock.js +411 -0
  94. package/app/logic/components/ProductLightbox.js +12 -0
  95. package/app/logic/components/ProductQuantity.js +31 -0
  96. package/app/logic/components/ProductVariantPicker.js +125 -0
  97. package/app/logic/components/QuickBuy.js +194 -0
  98. package/app/logic/components/SearchOverlay.js +41 -0
  99. package/app/logic/components/SidebarMenuItem.js +39 -0
  100. package/app/logic/components/SiteFooter.js +476 -0
  101. package/app/logic/components/SiteHeader.js +266 -0
  102. package/app/logic/components/WhatsAppButton.js +75 -0
  103. package/app/logic/layouts/default.js +66 -0
  104. package/app/logic/sections/AnnouncementBarSection.js +6 -0
  105. package/app/logic/sections/BlockCarousel.js +103 -0
  106. package/app/logic/sections/ClassificationListSection.js +40 -0
  107. package/app/logic/sections/ContactFormSection.js +294 -0
  108. package/app/logic/sections/ContactInfoSection.js +419 -0
  109. package/app/logic/sections/ContentNode.js +189 -0
  110. package/app/logic/sections/CountdownSection.js +254 -0
  111. package/app/logic/sections/CustomHtmlSection.js +51 -0
  112. package/app/logic/sections/DividerSection.js +31 -0
  113. package/app/logic/sections/FaqSection.js +53 -0
  114. package/app/logic/sections/FeatureListSection.js +6 -0
  115. package/app/logic/sections/FeaturedProductSection.js +40 -0
  116. package/app/logic/sections/FooterSection.js +16 -0
  117. package/app/logic/sections/FooterUtilitiesSection.js +24 -0
  118. package/app/logic/sections/GallerySection.js +232 -0
  119. package/app/logic/sections/HeaderSection.js +24 -0
  120. package/app/logic/sections/HeroSection.js +24 -0
  121. package/app/logic/sections/LogoListSection.js +6 -0
  122. package/app/logic/sections/MainClassificationSection.js +473 -0
  123. package/app/logic/sections/MainProductSection.js +211 -0
  124. package/app/logic/sections/MainSearchSection.js +6 -0
  125. package/app/logic/sections/MapSection.js +92 -0
  126. package/app/logic/sections/MediaContentSection.js +38 -0
  127. package/app/logic/sections/MediaWithTextSection.js +238 -0
  128. package/app/logic/sections/NewsletterSection.js +126 -0
  129. package/app/logic/sections/ProductHighlightSection.js +201 -0
  130. package/app/logic/sections/ProductListSection.js +45 -0
  131. package/app/logic/sections/ProductRecommendationsSection.js +113 -0
  132. package/app/logic/sections/RichTextSection.js +6 -0
  133. package/app/logic/sections/ScrollingTextSection.js +69 -0
  134. package/app/logic/sections/SearchHeaderSection.js +22 -0
  135. package/app/logic/sections/SlideshowSection.js +118 -0
  136. package/app/logic/sections/SpacerSection.js +37 -0
  137. package/app/logic/sections/SplitShowcaseSection.js +6 -0
  138. package/app/logic/sections/TestimonialListSection.js +6 -0
  139. package/app/logic/sections/VideoSection.js +235 -0
  140. package/app/pages/[category]/[product].vue +196 -0
  141. package/app/pages/__builder-preview.vue +38 -0
  142. package/app/pages/categories/[slug].vue +208 -0
  143. package/app/pages/checkout.vue +1972 -0
  144. package/app/pages/forgot-password.vue +115 -0
  145. package/app/pages/index.vue +64 -0
  146. package/app/pages/login.vue +585 -0
  147. package/app/pages/order-detail.vue +442 -0
  148. package/app/pages/pages/[handle].vue +58 -0
  149. package/app/pages/privacy-policy.vue +46 -0
  150. package/app/pages/resetpassword.vue +138 -0
  151. package/app/pages/resetverificationcode.vue +163 -0
  152. package/app/pages/result.vue +203 -0
  153. package/app/pages/return-policy.vue +50 -0
  154. package/app/pages/search.vue +100 -0
  155. package/app/pages/shipping-policy.vue +46 -0
  156. package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
  157. package/app/pages/user-profile/components/Whishlist.vue +137 -0
  158. package/app/pages/user-profile/components/left-bar.vue +233 -0
  159. package/app/pages/user-profile/components/orders.vue +162 -0
  160. package/app/pages/user-profile/components/profile-details.vue +566 -0
  161. package/app/pages/user-profile/components/settings.vue +194 -0
  162. package/app/pages/user-profile/index.vue +252 -0
  163. package/app/pages/verificationcode.vue +188 -0
  164. package/app/plugins/analytics.client.js +99 -0
  165. package/app/plugins/geo.ts +24 -0
  166. package/app/plugins/reveal.client.js +53 -0
  167. package/app/plugins/toast.js +10 -0
  168. package/app/plugins/vue-query.ts +5 -0
  169. package/app/services/ApiServices.js +74 -0
  170. package/app/services/AuthBus.js +162 -0
  171. package/app/services/Authentication/AuthenticationService.js +133 -0
  172. package/app/services/Authentication/TokenService.js +62 -0
  173. package/app/services/CartServices.js +91 -0
  174. package/app/services/CartToken.js +26 -0
  175. package/app/services/CategoryServices.js +54 -0
  176. package/app/services/CheckoutServices.js +113 -0
  177. package/app/services/HomeServices.js +133 -0
  178. package/app/services/NewsletterServices.js +29 -0
  179. package/app/services/PaymentServices.js +71 -0
  180. package/app/services/PoliciesServices.js +16 -0
  181. package/app/services/ProductServices.js +79 -0
  182. package/app/services/SearchServices.js +18 -0
  183. package/app/services/UserServices.js +105 -0
  184. package/app/services/UtilsServices.js +46 -0
  185. package/app/services/WishlistServices.js +51 -0
  186. package/app/services/storefront.service.js +57 -0
  187. package/app/stores/userStore.js +13 -0
  188. package/app/theme/categoryTree.js +68 -0
  189. package/app/theme/controls.js +88 -0
  190. package/app/theme/registry.js +56 -0
  191. package/app/utils/analytics.js +864 -0
  192. package/app/utils/device.js +50 -0
  193. package/app/utils/eventBus.js +24 -0
  194. package/app/utils/icon-presets.js +24 -0
  195. package/app/utils/productOffers.js +118 -0
  196. package/app/utils/requestCache.js +36 -0
  197. package/app/utils/scrollToError.js +59 -0
  198. package/app/utils/toPlainError.js +30 -0
  199. package/bin/osimart-theme.mjs +259 -0
  200. package/nuxt/osi-sections.ts +56 -0
  201. package/nuxt.config.ts +60 -0
  202. package/package.json +43 -3
  203. package/public/css/input.css +29 -0
  204. package/public/css/output.css +2716 -0
  205. package/public/favicon-32x32.png +0 -0
  206. 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
+