@mframework/layer-commerce 0.0.7 → 0.0.8

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/app/components/catalog/product/ProductAccordion/ProductAccordion.vue +41 -0
  2. package/app/components/catalog/product/ProductAccordion/__tests__/ProductAccordion.spec.ts +15 -0
  3. package/app/components/catalog/product/ProductAccordion/types.ts +5 -0
  4. package/app/components/catalog/product/ProductProperties/ProductProperties.vue +52 -0
  5. package/app/components/catalog/product/ProductProperties/__tests__/ProductProperties.spec.ts +15 -0
  6. package/app/components/catalog/product/ProductProperties/types.ts +5 -0
  7. package/app/components/catalog/product/ProductSlider/ProductSlider.vue +28 -0
  8. package/app/components/catalog/product/ProductSlider/__tests__/ProductSlider.spec.ts +14 -0
  9. package/app/components/catalog/product/ProductSlider/types.ts +7 -0
  10. package/app/components/catalog/product/RecommendedProducts/RecommendedProducts.vue +12 -0
  11. package/app/components/catalog/product/RecommendedProducts/types.ts +5 -0
  12. package/app/components/catalog/product/RenderContentProductSlider/RenderContentProductSlider.vue +11 -0
  13. package/app/components/catalog/product/add-attribute.vue +54 -0
  14. package/app/components/catalog/product/add-product-type.vue +54 -0
  15. package/app/components/catalog/product/add-product.vue +53 -0
  16. package/app/components/catalog/product/add-showcase.vue +52 -0
  17. package/app/components/catalog/product/add-station.vue +54 -0
  18. package/app/components/catalog/product/bestsellers.vue +69 -0
  19. package/app/components/catalog/product/bidding.vue +93 -0
  20. package/app/components/catalog/product/colorOptions.vue +58 -0
  21. package/app/components/catalog/product/deals.vue +61 -0
  22. package/app/components/catalog/product/exclusives.vue +58 -0
  23. package/app/components/catalog/product/featuredproducts.vue +69 -0
  24. package/app/components/catalog/product/giftCard.vue +63 -0
  25. package/app/components/catalog/product/latestproducts.vue +58 -0
  26. package/app/components/catalog/product/productCard.vue +71 -0
  27. package/app/components/catalog/product/productCompare.vue +60 -0
  28. package/app/components/catalog/product/productCompareTable.vue +441 -0
  29. package/app/components/catalog/product/productDetails.vue +120 -0
  30. package/app/components/catalog/product/productFaqs.vue +17 -0
  31. package/app/components/catalog/product/productGallery.vue +16 -0
  32. package/app/components/catalog/product/productQty.vue +54 -0
  33. package/app/components/catalog/product/productReviews.vue +56 -0
  34. package/app/components/catalog/product/productSpecs.vue +116 -0
  35. package/app/components/catalog/product/radiostation.vue +36 -0
  36. package/app/components/catalog/product/recentlyviewed.vue +43 -0
  37. package/app/components/catalog/product/relatedbrands.vue +54 -0
  38. package/app/components/catalog/product/relatedproducts.vue +58 -0
  39. package/app/components/catalog/product/relatedstations.vue +40 -0
  40. package/app/components/catalog/product/shippingOptions.vue +41 -0
  41. package/app/components/catalog/product/sizeOptions.vue +47 -0
  42. package/app/components/catalog/product/update-attribute-set.vue +209 -0
  43. package/app/components/catalog/product/update-attribute.vue +118 -0
  44. package/app/components/catalog/product/update-product.vue +372 -0
  45. package/app/components/catalog/product/update-showcase.vue +153 -0
  46. package/app/components/catalog/shops/relatedstores.vue +52 -0
  47. package/app/components/catalog/shops/restaurant.vue +66 -0
  48. package/app/components/catalog/shops/stores.vue +44 -0
  49. package/app/components/catalog/vendor/README.md +3 -0
  50. package/app/components/catalog/vendor/blocks/biggestcustomers.vue +33 -0
  51. package/app/components/catalog/vendor/blocks/lowestselling.vue +33 -0
  52. package/app/components/catalog/vendor/blocks/topcategories.vue +33 -0
  53. package/app/components/catalog/vendor/blocks/topproducts.vue +27 -0
  54. package/app/components/catalog/vendor/pages/attributes.vue +43 -0
  55. package/app/components/catalog/vendor/pages/commissions.vue +43 -0
  56. package/app/components/catalog/vendor/pages/crm.vue +67 -0
  57. package/app/components/catalog/vendor/pages/dashboard.vue +46 -0
  58. package/app/components/catalog/vendor/pages/emails.vue +43 -0
  59. package/app/components/catalog/vendor/pages/enquiries.vue +43 -0
  60. package/app/components/catalog/vendor/pages/invoices.vue +43 -0
  61. package/app/components/catalog/vendor/pages/orders.vue +68 -0
  62. package/app/components/catalog/vendor/pages/products.vue +55 -0
  63. package/app/components/catalog/vendor/pages/reviews.vue +48 -0
  64. package/app/components/catalog/vendor/pages/shipments.vue +43 -0
  65. package/app/components/catalog/vendor/pages/stores.vue +43 -0
  66. package/app/components/content/blocks/breadcrumbs.vue +0 -0
  67. package/app/components/content/blocks/currencySwitcher.vue +0 -0
  68. package/app/components/content/blocks/languageSwitcher.vue +0 -0
  69. package/app/components/content/blocks/videoproduct.vue +9 -0
  70. package/app/components/content/pages/checkout.vue +118 -0
  71. package/app/components/content/pages/meeoviGlobal.vue +68 -0
  72. package/app/components/content/pages/pickup-locations.vue +238 -0
  73. package/app/components/content/pages/showcases.vue +90 -0
  74. package/app/components/content/pages/success.vue +60 -0
  75. package/app/components/marketing/add-brand.vue +54 -0
  76. package/app/components/marketing/add-incentive.vue +54 -0
  77. package/app/components/marketing/promotions/giftcards.vue +127 -0
  78. package/app/components/marketing/promotions/subscriptions.vue +134 -0
  79. package/app/components/marketing/update-incentive.vue +326 -0
  80. package/app/components/menus/lowernav.vue +78 -0
  81. package/app/components/partials/LocaleSelector.vue +24 -0
  82. package/app/components/partials/ShoppingCart.vue +128 -0
  83. package/app/components/partials/StripePayment.vue +149 -0
  84. package/app/components/partials/addToCartBtn.vue +40 -0
  85. package/app/components/partials/cartItem.vue +124 -0
  86. package/app/components/partials/checkoutButton.vue +44 -0
  87. package/app/components/partials/compareBtn.vue +68 -0
  88. package/app/components/partials/ratings.vue +13 -0
  89. package/app/components/partials/store/CurrencySelector.vue +133 -0
  90. package/app/components/partials/store/StoreSwitcher.vue +13 -0
  91. package/app/components/related/brandCard.vue +41 -0
  92. package/app/components/related/incentiveCard.vue +44 -0
  93. package/app/components/related/invoiceCard.vue +43 -0
  94. package/app/components/related/orderCard.vue +43 -0
  95. package/app/components/related/relatedproducts.vue +17 -0
  96. package/app/components/sales/CartPageContent/CartPageContent.vue +43 -0
  97. package/app/components/sales/CheckoutAddress/CheckoutAddress.vue +50 -0
  98. package/app/components/sales/CheckoutAddress/__tests__/CheckoutAddress.spec.ts +16 -0
  99. package/app/components/sales/CheckoutAddress/types.ts +16 -0
  100. package/app/components/sales/CheckoutPayment/CheckoutPayment.vue +65 -0
  101. package/app/components/sales/CheckoutPayment/__tests__/CheckoutPayment.spec.ts +14 -0
  102. package/app/components/sales/CheckoutPayment/types.ts +12 -0
  103. package/app/components/sales/OrderSummary/OrderSummary.vue +57 -0
  104. package/app/components/sales/OrderSummary/__tests__/ContactInformation.spec.ts +52 -0
  105. package/app/components/sales/OrderSummary/types.ts +5 -0
  106. package/app/components/sales/incentives.vue +247 -0
  107. package/app/components/sales/invoices.vue +107 -0
  108. package/app/components/sales/orders.vue +378 -0
  109. package/app/components/sales/shipments.vue +65 -0
  110. package/app/components/sales/transactions.vue +109 -0
  111. package/app/components/shop/add-shop.vue +54 -0
  112. package/app/components/shop/cart/cartItem.vue +182 -0
  113. package/app/components/shop/cart/checkout.vue +415 -0
  114. package/app/components/shop/checkout/StripeCardElement.vue +206 -0
  115. package/app/components/shop/checkout/StripeCheckout.vue +49 -0
  116. package/app/components/shop/checkout/addressBilling.vue +263 -0
  117. package/app/components/shop/checkout/addressShipping.vue +175 -0
  118. package/app/components/shop/checkout/cart/ProductItem.vue +56 -0
  119. package/app/components/shop/checkout/cart/PromotionItem.vue +53 -0
  120. package/app/stores/cart.ts +1 -1
  121. package/app/types/Direction.type.ts +1 -1
  122. package/app/types/Global.type.ts +6 -6
  123. package/app/types/Layout.type.ts +1 -1
  124. package/app/{normalizers → types/normalizers}/Cart.query.ts +1 -1
  125. package/app/{normalizers → types/normalizers}/Cart.type.ts +2 -2
  126. package/app/{normalizers → types/normalizers}/Checkout.query.ts +2 -2
  127. package/app/{normalizers → types/normalizers}/Config.query.ts +1 -1
  128. package/app/{normalizers → types/normalizers}/Config.type.ts +1 -1
  129. package/app/{normalizers → types/normalizers}/ContactForm.query.ts +2 -2
  130. package/app/{normalizers → types/normalizers}/CreditMemo.type.ts +1 -1
  131. package/app/{normalizers → types/normalizers}/GiftCard.type.ts +1 -1
  132. package/app/{normalizers → types/normalizers}/Invoice.type.ts +1 -1
  133. package/app/{normalizers → types/normalizers}/MyAccount.query.ts +1 -1
  134. package/app/{normalizers → types/normalizers}/MyAccount.type.ts +1 -1
  135. package/app/{normalizers → types/normalizers}/NewsletterSubscription.query.ts +1 -1
  136. package/app/{normalizers → types/normalizers}/Order.query.ts +1 -1
  137. package/app/{normalizers → types/normalizers}/Order.type.ts +2 -2
  138. package/app/{normalizers → types/normalizers}/Payment.type.ts +1 -1
  139. package/app/{normalizers → types/normalizers}/ProductCompare.query.ts +1 -1
  140. package/app/{normalizers → types/normalizers}/ProductCompare.type.ts +1 -1
  141. package/app/{normalizers → types/normalizers}/ProductList.query.ts +2 -2
  142. package/app/{normalizers → types/normalizers}/ProductList.type.ts +2 -2
  143. package/app/{normalizers → types/normalizers}/Return.type.ts +1 -1
  144. package/app/{normalizers → types/normalizers}/Review.query.ts +1 -1
  145. package/app/{normalizers → types/normalizers}/Review.type.ts +1 -1
  146. package/app/{normalizers → types/normalizers}/StoreInPickUp.query.ts +1 -1
  147. package/app/{normalizers → types/normalizers}/Subscription.type.ts +1 -1
  148. package/app/{normalizers → types/normalizers}/Transaction.type.ts +1 -1
  149. package/app/{normalizers → types/normalizers}/UrlRewrites.query.ts +1 -1
  150. package/app/{normalizers → types/normalizers}/UrlRewrites.type.ts +1 -1
  151. package/app/{normalizers → types/normalizers}/Wishlist.query.ts +4 -4
  152. package/app/{normalizers → types/normalizers}/Wishlist.type.ts +1 -1
  153. package/app/utils/Address/Address.type.ts +1 -1
  154. package/app/utils/Address/index.ts +5 -5
  155. package/app/utils/Cart/Cart.ts +1 -1
  156. package/app/utils/Currency/Currency.ts +1 -1
  157. package/app/utils/History/History.type.ts +1 -1
  158. package/app/utils/Menu/Menu.ts +1 -1
  159. package/app/utils/Menu/Menu.type.ts +2 -2
  160. package/app/utils/Orders/Orders.ts +1 -1
  161. package/app/utils/Preload/CategoryPreload.ts +2 -2
  162. package/app/utils/Preload/ProductPreload.ts +1 -1
  163. package/app/utils/Preload/index.ts +1 -1
  164. package/app/utils/Price/Price.ts +1 -1
  165. package/app/utils/Product/Extract.ts +1 -1
  166. package/app/utils/Product/Product.ts +1 -1
  167. package/app/utils/Product/Product.type.ts +1 -1
  168. package/app/utils/Product/Transform.ts +1 -1
  169. package/app/utils/Wishlist/Wishlist.ts +1 -1
  170. package/app/utils/client.ts +19 -19
  171. package/package.json +1 -2
  172. package/tsconfig.json +2 -2
  173. package/app/cart/useCart.ts +0 -1
  174. /package/app/{components → composables}/ChevronIcon/ChevronIcon.config.ts +0 -0
  175. /package/app/{components → composables}/DateSelect/DateSelect.config.ts +0 -0
  176. /package/app/{components → composables}/Field/Field.config.ts +0 -0
  177. /package/app/{components → composables}/FieldDate/FieldDate.config.ts +0 -0
  178. /package/app/{components → composables}/Form/Form.type.ts +0 -0
  179. /package/app/{components → composables}/Product/Product.config.ts +0 -0
  180. /package/app/{components → composables}/Product/Stock.config.ts +0 -0
  181. /package/app/{components → composables}/ProductCustomizableOption/ProductCustomizableOption.config.ts +0 -0
  182. /package/app/{components → composables}/ProductGallery/ProductGallery.config.ts +0 -0
  183. /package/app/{components → composables}/ProductReviews/ProductReviews.config.ts +0 -0
  184. /package/app/{normalizers → types/normalizers}/Category.query.ts +0 -0
  185. /package/app/{normalizers → types/normalizers}/Category.type.ts +0 -0
  186. /package/app/{normalizers → types/normalizers}/CheckEmail.query.ts +0 -0
  187. /package/app/{normalizers → types/normalizers}/Checkout.type.ts +0 -0
  188. /package/app/{normalizers → types/normalizers}/CmsBlock.query.ts +0 -0
  189. /package/app/{normalizers → types/normalizers}/CmsBlock.type.ts +0 -0
  190. /package/app/{normalizers → types/normalizers}/CmsPage.query.ts +0 -0
  191. /package/app/{normalizers → types/normalizers}/CmsPage.type.ts +0 -0
  192. /package/app/{normalizers → types/normalizers}/Menu.query.ts +0 -0
  193. /package/app/{normalizers → types/normalizers}/Menu.type.ts +0 -0
  194. /package/app/{normalizers → types/normalizers}/ProductAlerts.query.ts +0 -0
  195. /package/app/{normalizers → types/normalizers}/Region.query.ts +0 -0
  196. /package/app/{normalizers → types/normalizers}/Region.type.ts +0 -0
  197. /package/app/{normalizers → types/normalizers}/Slider.query.ts +0 -0
  198. /package/app/{normalizers → types/normalizers}/Slider.type.ts +0 -0
  199. /package/app/{normalizers → types/normalizers}/StoreInPickUp.type.ts +0 -0
  200. /package/app/{routes → types/routes}/CategoryPage/CategoryPage.config.ts +0 -0
  201. /package/app/{routes → types/routes}/CategoryPage/CategoryPage.type.ts +0 -0
  202. /package/app/{routes → types/routes}/Checkout/Checkout.config.ts +0 -0
  203. /package/app/{routes → types/routes}/Checkout/Checkout.type.ts +0 -0
  204. /package/app/{routes → types/routes}/MyAccount/MyAccount.config.ts +0 -0
  205. /package/app/{routes → types/routes}/SearchPage/SearchPage.config.ts +0 -0
  206. /package/app/{routes → types/routes}/UrlRewrites/UrlRewrites.config.ts +0 -0
@@ -0,0 +1,206 @@
1
+ <template>
2
+ <v-form @submit.prevent="handlePayment" aria-label="Stripe payment form" ref="formRef">
3
+ <div ref="cardElement" class="StripeElement" aria-label="Card input"></div>
4
+ <div v-if="cardError" class="stripe-error" role="alert">{{ cardError }}</div>
5
+ <v-row class="stripe-details">
6
+ <v-col cols="12" md="6">
7
+ <v-text-field v-model="billingDetails.name" label="Name on Card" required autocomplete="cc-name" outlined />
8
+ </v-col>
9
+ <v-col cols="12" md="6">
10
+ <v-text-field v-model="billingDetails.email" label="Email" type="email" required autocomplete="email" outlined />
11
+ </v-col>
12
+ <v-col cols="12" md="6">
13
+ <v-text-field v-model="billingDetails.phone" label="Phone" type="tel" autocomplete="tel" outlined />
14
+ </v-col>
15
+ <v-col cols="12" md="6">
16
+ <v-select v-model="billingDetails.address.country" :items="countryOptions" label="Country" required outlined />
17
+ </v-col>
18
+ <v-col cols="12" md="6">
19
+ <v-text-field v-model="billingDetails.address.line1" label="Address" required outlined />
20
+ </v-col>
21
+ <v-col cols="12" md="6">
22
+ <v-text-field v-model="billingDetails.address.city" label="City" required outlined />
23
+ </v-col>
24
+ <v-col cols="12" md="6">
25
+ <v-text-field v-model="billingDetails.address.state" label="State" required outlined />
26
+ </v-col>
27
+ <v-col cols="12" md="6">
28
+ <v-text-field v-model="billingDetails.address.postal_code" label="Postal Code" required outlined />
29
+ </v-col>
30
+ </v-row>
31
+ <v-row class="stripe-details">
32
+ <v-col cols="12" md="6">
33
+ <v-text-field v-model="shippingDetails.name" label="Shipping Name" required outlined />
34
+ </v-col>
35
+ <v-col cols="12" md="6">
36
+ <v-select v-model="shippingDetails.address.country" :items="countryOptions" label="Shipping Country" required outlined />
37
+ </v-col>
38
+ <v-col cols="12" md="6">
39
+ <v-text-field v-model="shippingDetails.address.line1" label="Shipping Address" required outlined />
40
+ </v-col>
41
+ <v-col cols="12" md="6">
42
+ <v-text-field v-model="shippingDetails.address.city" label="Shipping City" required outlined />
43
+ </v-col>
44
+ <v-col cols="12" md="6">
45
+ <v-text-field v-model="shippingDetails.address.state" label="Shipping State" required outlined />
46
+ </v-col>
47
+ <v-col cols="12" md="6">
48
+ <v-text-field v-model="shippingDetails.address.postal_code" label="Shipping Postal Code" required outlined />
49
+ </v-col>
50
+ </v-row>
51
+ <v-checkbox v-model="saveCard" label="Save card for future payments (secure)" class="mb-4" />
52
+ <v-btn type="submit" :disabled="loading || !cardReady" aria-busy="loading">
53
+ <span v-if="loading">Processing...</span>
54
+ <span v-else>Pay</span>
55
+ </v-btn>
56
+ </v-form>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
61
+ import { v-btn } from '@storefront-ui/vue';
62
+ import { useUserStore } from '../../stores/user';
63
+ import { useDirectusAuth } from '../../../../../composables/useDirectusAuth';
64
+ import countryList from '../../utils/countryList'; // Assume this is an array of country codes/names
65
+
66
+ const props = defineProps<{ clientSecret: string }>();
67
+ const emit = defineEmits(['payment-success', 'payment-error']);
68
+
69
+ const cardElement = ref<HTMLElement | null>(null);
70
+ let card: any = null;
71
+ let elements: any = null;
72
+ const loading = ref(false);
73
+ const cardError = ref('');
74
+ const cardReady = ref(false);
75
+ const saveCard = ref(false);
76
+ const formRef = ref();
77
+
78
+ const userStore = useUserStore();
79
+ const directusAuth = useDirectusAuth();
80
+ const directusUser = computed(() => directusAuth.user || {});
81
+
82
+ // Prefer Directus user data if available, fallback to store
83
+ const user = computed(() => Object.keys(directusUser.value).length ? directusUser.value : userStore.user || {});
84
+
85
+ const countryOptions = countryList.map((c: any) => ({ text: c.name, value: c.code }));
86
+
87
+ const billingDetails = ref({
88
+ name: user.value.name || '',
89
+ email: user.value.email || '',
90
+ phone: user.value.phone || '',
91
+ address: {
92
+ line1: user.value.address?.line1 || '',
93
+ city: user.value.address?.city || '',
94
+ state: user.value.address?.state || '',
95
+ postal_code: user.value.address?.postal_code || '',
96
+ country: user.value.address?.country || '',
97
+ },
98
+ });
99
+ const shippingDetails = ref({
100
+ name: user.value.shippingName || user.value.name || '',
101
+ address: {
102
+ line1: user.value.shippingAddress?.line1 || '',
103
+ city: user.value.shippingAddress?.city || '',
104
+ state: user.value.shippingAddress?.state || '',
105
+ postal_code: user.value.shippingAddress?.postal_code || '',
106
+ country: user.value.shippingAddress?.country || '',
107
+ },
108
+ });
109
+
110
+ function useNuxtApp() {
111
+ // Return a simple wrapper exposing a client-side Stripe instance if available
112
+ return {
113
+ $stripe: (typeof window !== 'undefined' && window.Stripe) ? window.Stripe : null
114
+ };
115
+ }
116
+ const { $stripe } = useNuxtApp();
117
+
118
+ onMounted(() => {
119
+ if (!$stripe || !props.clientSecret) return;
120
+ elements = $stripe.elements({ clientSecret: props.clientSecret });
121
+ card = elements.create('card');
122
+ card.mount(cardElement.value);
123
+ card.on('change', (event: any) => {
124
+ cardError.value = event.error ? event.error.message : '';
125
+ cardReady.value = event.complete;
126
+ });
127
+ });
128
+
129
+ onUnmounted(() => {
130
+ if (card) card.destroy();
131
+ });
132
+
133
+ watch(() => props.clientSecret, (newSecret) => {
134
+ if (elements && newSecret) {
135
+ // Re-create card element if clientSecret changes
136
+ if (card) card.destroy();
137
+ card = elements.create('card');
138
+ card.mount(cardElement.value);
139
+ card.on('change', (event: any) => {
140
+ cardError.value = event.error ? event.error.message : '';
141
+ cardReady.value = event.complete;
142
+ });
143
+ }
144
+ });
145
+
146
+ const handlePayment = async () => {
147
+ cardError.value = '';
148
+ loading.value = true;
149
+ try {
150
+ if (!$stripe || !props.clientSecret) {
151
+ cardError.value = 'Stripe is not initialized.';
152
+ emit('payment-error', cardError.value);
153
+ return;
154
+ }
155
+ // Validate form before submitting
156
+ if (formRef.value && !(await formRef.value.validate?.())) {
157
+ loading.value = false;
158
+ return;
159
+ }
160
+ const { error, paymentIntent, setupIntent } = await $stripe.confirmCardPayment(props.clientSecret, {
161
+ payment_method: {
162
+ card: card,
163
+ billing_details: billingDetails.value,
164
+ },
165
+ shipping: shippingDetails.value,
166
+ setup_future_usage: saveCard.value ? 'off_session' : undefined, // Save card if checked
167
+ // 3D Secure is handled automatically by Stripe if required
168
+ });
169
+ if (error) {
170
+ cardError.value = error.message || 'Payment failed.';
171
+ emit('payment-error', cardError.value);
172
+ return;
173
+ }
174
+ if (paymentIntent && paymentIntent.status === 'succeeded') {
175
+ emit('payment-success', paymentIntent);
176
+ } else if (setupIntent && setupIntent.status === 'succeeded') {
177
+ emit('payment-success', setupIntent);
178
+ } else {
179
+ cardError.value = 'Payment was not successful.';
180
+ emit('payment-error', cardError.value);
181
+ }
182
+ } catch (e: any) {
183
+ cardError.value = e?.message || 'An unexpected error occurred.';
184
+ emit('payment-error', cardError.value);
185
+ } finally {
186
+ loading.value = false;
187
+ }
188
+ };
189
+ </script>
190
+
191
+ <style scoped>
192
+ .StripeElement {
193
+ border: 1px solid #ccc;
194
+ padding: 10px;
195
+ border-radius: 4px;
196
+ margin-bottom: 1rem;
197
+ }
198
+ .stripe-error {
199
+ color: #d32f2f;
200
+ margin-bottom: 1rem;
201
+ font-size: 0.95em;
202
+ }
203
+ .stripe-details {
204
+ margin-bottom: 1rem;
205
+ }
206
+ </style>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <div>
3
+ <h1>Checkout</h1>
4
+ <v-btn @click="startCheckout" :disabled="loading || !clientSecret">
5
+ Pay with Stripe
6
+ </v-btn>
7
+ <StripeCardElement v-if="clientSecret" :clientSecret="clientSecret" @payment-success="onPaymentSuccess" />
8
+ </div>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { ref } from 'vue';
13
+ import { v-btn } from '@storefront-ui/vue';
14
+ import { useVendureQuery } from '@/app/composables/useVendureQuery';
15
+ import { useVendureMutation } from '@/app/composables/useVendureMutation';
16
+ import createStripePaymentIntentMutation from '#graphql/app/commerce/mutations/createStripePaymentIntent.gql';
17
+ import getActiveOrderQuery from '#graphql/app/commerce/queries/getActiveOrder.gql';
18
+ import StripeCardElement from './StripeCardElement.vue';
19
+
20
+ const loading = ref(false);
21
+ const clientSecret = ref<string | null>(null);
22
+
23
+ // Get the active order (cart)
24
+ const { data: orderData, refetch } = useVendureQuery(getActiveOrderQuery);
25
+
26
+ const { mutate: createStripePaymentIntent } = useVendureMutation(createStripePaymentIntentMutation);
27
+
28
+ const startCheckout = async () => {
29
+ if (!orderData.value?.activeOrder) return;
30
+ loading.value = true;
31
+ try {
32
+ // Create payment intent on the server via Vendure mutation
33
+ const response = await createStripePaymentIntent({
34
+ orderId: orderData.value.activeOrder.id,
35
+ });
36
+ clientSecret.value = response?.createStripePaymentIntent?.clientSecret;
37
+ } catch (err) {
38
+ // Handle error (show notification, etc.)
39
+ console.error(err);
40
+ } finally {
41
+ loading.value = false;
42
+ }
43
+ };
44
+
45
+ const onPaymentSuccess = () => {
46
+ // Optionally refetch order/cart, show success notification, etc.
47
+ refetch();
48
+ };
49
+ </script>
@@ -0,0 +1,263 @@
1
+ <template>
2
+ <v-form class="pa-4" @submit.prevent="setBillingAddressHandler">
3
+ <!-- Loading Overlay -->
4
+ <v-overlay
5
+ :model-value="loading"
6
+ class="align-center justify-center"
7
+ >
8
+ <v-progress-circular
9
+ indeterminate
10
+ size="64"
11
+ ></v-progress-circular>
12
+ </v-overlay>
13
+
14
+ <!-- Error Alert -->
15
+ <v-alert
16
+ v-if="error"
17
+ type="error"
18
+ closable
19
+ class="mb-4"
20
+ @click:close="error = null"
21
+ >
22
+ {{ error }}
23
+ </v-alert>
24
+
25
+ <h2 class="text-h4 font-weight-bold mb-4">Billing Address</h2>
26
+ <v-row>
27
+ <v-col cols="12" md="6">
28
+ <v-text-field
29
+ v-model="billingAddress.fullName"
30
+ :error-messages="errors.fullName"
31
+ label="Full Name"
32
+ name="fullName"
33
+ autocomplete="name"
34
+ required
35
+ variant="outlined"
36
+ ></v-text-field>
37
+ </v-col>
38
+ <v-col cols="12" md="6">
39
+ <v-text-field
40
+ v-model="billingAddress.phoneNumber"
41
+ :error-messages="errors.phoneNumber"
42
+ label="Phone"
43
+ name="phone"
44
+ type="tel"
45
+ autocomplete="tel"
46
+ required
47
+ variant="outlined"
48
+ ></v-text-field>
49
+ </v-col>
50
+ <v-col cols="12">
51
+ <v-select
52
+ v-model="billingAddress.countryCode"
53
+ :error-messages="errors.countryCode"
54
+ label="Country"
55
+ name="country"
56
+ :items="countries"
57
+ item-title="name"
58
+ item-value="code"
59
+ autocomplete="country-name"
60
+ required
61
+ variant="outlined"
62
+ @update:model-value="updateRegions"
63
+ ></v-select>
64
+ </v-col>
65
+ <v-col cols="12" md="8">
66
+ <v-text-field
67
+ v-model="billingAddress.streetLine1"
68
+ :error-messages="errors.streetLine1"
69
+ label="Street Address 1"
70
+ name="streetLine1"
71
+ autocomplete="address-line1"
72
+ required
73
+ variant="outlined"
74
+ hint="Street address or P.O. Box"
75
+ ></v-text-field>
76
+ </v-col>
77
+ <v-col cols="12" md="4">
78
+ <v-text-field
79
+ v-model="billingAddress.streetLine2"
80
+ label="Street Address 2"
81
+ name="streetLine2"
82
+ variant="outlined"
83
+ hint="Optional"
84
+ ></v-text-field>
85
+ </v-col>
86
+ <v-col cols="12" md="4">
87
+ <v-text-field
88
+ v-model="billingAddress.city"
89
+ :error-messages="errors.city"
90
+ label="City"
91
+ name="city"
92
+ autocomplete="address-level2"
93
+ required
94
+ variant="outlined"
95
+ ></v-text-field>
96
+ </v-col>
97
+ <v-col cols="12" md="4">
98
+ <v-select
99
+ v-model="billingAddress.province"
100
+ :error-messages="errors.province"
101
+ label="State/Province"
102
+ name="province"
103
+ :items="regions"
104
+ item-title="name"
105
+ item-value="code"
106
+ autocomplete="address-level1"
107
+ required
108
+ variant="outlined"
109
+ ></v-select>
110
+ </v-col>
111
+ <v-col cols="12" md="4">
112
+ <v-text-field
113
+ v-model="billingAddress.postalCode"
114
+ :error-messages="errors.postalCode"
115
+ label="ZIP/Postal Code"
116
+ name="postalCode"
117
+ placeholder="eg. 12345"
118
+ autocomplete="postal-code"
119
+ required
120
+ variant="outlined"
121
+ ></v-text-field>
122
+ </v-col>
123
+ <v-col cols="12">
124
+ <v-checkbox
125
+ v-model="useAsShippingAddress"
126
+ label="Use as shipping address"
127
+ name="useAsShippingAddress"
128
+ ></v-checkbox>
129
+ </v-col>
130
+ <v-col cols="12" class="d-flex justify-end gap-4">
131
+ <v-btn
132
+ variant="outlined"
133
+ type="reset"
134
+ color="primary"
135
+ @click="handleReset"
136
+ >
137
+ Clear all
138
+ </v-btn>
139
+ <v-btn
140
+ color="primary"
141
+ type="submit"
142
+ :loading="loading"
143
+ :disabled="loading"
144
+ >
145
+ Save
146
+ </v-btn>
147
+ </v-col>
148
+ </v-row>
149
+ </v-form>
150
+ </template>
151
+
152
+ <script setup lang="ts">
153
+ import { ref, reactive, onMounted, onErrorCaptured } from 'vue';
154
+ import { useVendureMutation } from '../../composables/useVendureMutation';
155
+ import setOrderBillingAddressMutation from '#graphql/app/commerce/mutations/setOrderBillingAddress.gql';
156
+ import getCountryListQuery from '#graphql/app/commerce/queries/getCountryList.gql';
157
+ import { useVendureQuery } from '../../composables/useVendureQuery';
158
+ import { useNotification } from '~//composables/useNotifications';
159
+
160
+ const emit = defineEmits(['address-saved', 'address-error', 'form-reset']);
161
+ const { showNotification } = useNotification();
162
+ const loading = ref(false);
163
+ const error = ref<string | null>(null);
164
+ const useAsShippingAddress = ref(false);
165
+
166
+ // Add a reactive errors object for field-level error messages
167
+ const errors = reactive({
168
+ fullName: '',
169
+ streetLine1: '',
170
+ streetLine2: '',
171
+ city: '',
172
+ province: '',
173
+ postalCode: '',
174
+ phoneNumber: '',
175
+ countryCode: '',
176
+ });
177
+
178
+ const { data: countriesResult } = useVendureQuery(getCountryListQuery);
179
+ type Country = {
180
+ code: string;
181
+ name: string;
182
+ regions?: Array<{ code: string; name: string }>;
183
+ };
184
+
185
+ const countries = ref<Country[]>([]);
186
+ const regions = ref<Array<{ code: string; name: string }>>([]);
187
+
188
+ const billingAddress = reactive({
189
+ fullName: '',
190
+ streetLine1: '',
191
+ streetLine2: '',
192
+ city: '',
193
+ province: '',
194
+ postalCode: '',
195
+ phoneNumber: '',
196
+ countryCode: '',
197
+ });
198
+
199
+ const { mutate: setBillingAddress } = useVendureMutation(setOrderBillingAddressMutation);
200
+
201
+ const updateRegions = (countryCode: string) => {
202
+ const country = countries.value.find((c) => c.code === countryCode);
203
+ regions.value = country?.regions || [];
204
+ };
205
+
206
+ const handleReset = () => {
207
+ Object.assign(billingAddress, {
208
+ fullName: '',
209
+ streetLine1: '',
210
+ streetLine2: '',
211
+ city: '',
212
+ province: '',
213
+ postalCode: '',
214
+ phoneNumber: '',
215
+ countryCode: '',
216
+ });
217
+ useAsShippingAddress.value = false;
218
+ emit('form-reset');
219
+ };
220
+
221
+ const setBillingAddressHandler = async () => {
222
+ loading.value = true;
223
+ error.value = null;
224
+ try {
225
+ const result = await setBillingAddress({
226
+ input: {
227
+ fullName: billingAddress.fullName,
228
+ streetLine1: billingAddress.streetLine1,
229
+ streetLine2: billingAddress.streetLine2,
230
+ city: billingAddress.city,
231
+ province: billingAddress.province,
232
+ postalCode: billingAddress.postalCode,
233
+ phoneNumber: billingAddress.phoneNumber,
234
+ countryCode: billingAddress.countryCode,
235
+ },
236
+ });
237
+ showNotification({ type: 'success', message: 'Billing address updated' });
238
+ emit('address-saved', result?.setOrderBillingAddress?.billingAddress);
239
+ } catch (err) {
240
+ error.value = err instanceof Error ? err.message : 'Failed to update billing address';
241
+ showNotification({ type: 'error', message: error.value });
242
+ emit('address-error', err instanceof Error ? err : new Error(error.value));
243
+ } finally {
244
+ loading.value = false;
245
+ }
246
+ };
247
+
248
+ onMounted(() => {
249
+ if (countriesResult.value) {
250
+ countries.value = countriesResult.value.countries;
251
+ }
252
+ });
253
+ onErrorCaptured((err) => {
254
+ error.value = err instanceof Error ? err.message : 'An unexpected error occurred';
255
+ return false;
256
+ });
257
+ </script>
258
+
259
+ <style scoped>
260
+ .gap-4 {
261
+ gap: 1rem;
262
+ }
263
+ </style>
@@ -0,0 +1,175 @@
1
+ <template>
2
+ <v-form class="pa-4" @submit.prevent="setShippingAddressHandler">
3
+ <!-- Loading Overlay -->
4
+ <v-overlay :model-value="loading" class="align-center justify-center">
5
+ <v-progress-circular indeterminate size="64"></v-progress-circular>
6
+ </v-overlay>
7
+
8
+ <!-- Error Alert -->
9
+ <v-alert v-if="error" type="error" closable class="mb-4" @click:close="error = null">
10
+ {{ error }}
11
+ </v-alert>
12
+
13
+ <h2 class="text-h4 font-weight-bold mb-4">Shipping Address</h2>
14
+ <v-row>
15
+ <v-col cols="12" md="6">
16
+ <v-text-field v-model="shippingAddress.fullName" :error-messages="errors.fullName" label="Full Name"
17
+ name="fullName" autocomplete="name" required variant="outlined"></v-text-field>
18
+ </v-col>
19
+ <v-col cols="12" md="6">
20
+ <v-text-field v-model="shippingAddress.phoneNumber" :error-messages="errors.phoneNumber" label="Phone"
21
+ name="phone" type="tel" autocomplete="tel" required variant="outlined"></v-text-field>
22
+ </v-col>
23
+ <v-col cols="12">
24
+ <v-select v-model="shippingAddress.countryCode" :error-messages="errors.countryCode" label="Country"
25
+ name="country" :items="countries" item-title="name" item-value="code"
26
+ autocomplete="country-name" required variant="outlined"
27
+ @update:model-value="updateRegions"></v-select>
28
+ </v-col>
29
+ <v-col cols="12" md="8">
30
+ <v-text-field v-model="shippingAddress.streetLine1" :error-messages="errors.streetLine1" label="Street Line 1"
31
+ name="streetLine1" autocomplete="address-line1" required variant="outlined"
32
+ hint="Street address or P.O. Box"></v-text-field>
33
+ </v-col>
34
+ <v-col cols="12" md="4">
35
+ <v-text-field v-model="shippingAddress.streetLine2" label="Street Line 2" name="streetLine2"
36
+ variant="outlined" hint="Optional"></v-text-field>
37
+ </v-col>
38
+ <v-col cols="12" md="4">
39
+ <v-text-field v-model="shippingAddress.city" :error-messages="errors.city" label="City" name="city"
40
+ autocomplete="address-level2" required variant="outlined"></v-text-field>
41
+ </v-col>
42
+ <v-col cols="12" md="4">
43
+ <v-select v-model="shippingAddress.province" :error-messages="errors.province" label="State" name="state"
44
+ :items="regions" item-title="name" item-value="code" autocomplete="address-level1" required
45
+ variant="outlined"></v-select>
46
+ </v-col>
47
+ <v-col cols="12" md="4">
48
+ <v-text-field v-model="shippingAddress.postalCode" :error-messages="errors.postalCode" label="ZIP Code"
49
+ name="zipCode" placeholder="eg. 12345" autocomplete="postal-code" required
50
+ variant="outlined"></v-text-field>
51
+ </v-col>
52
+ <v-col cols="12">
53
+ <v-checkbox v-model="useAsShippingAddress" label="Use as shipping address"
54
+ name="useAsShippingAddress"></v-checkbox>
55
+ </v-col>
56
+ <v-col cols="12" class="d-flex justify-end gap-4">
57
+ <v-btn variant="outlined" type="reset" color="primary" @click="handleReset">
58
+ Clear all
59
+ </v-btn>
60
+ <v-btn color="primary" type="submit" :loading="loading" :disabled="loading">
61
+ Save
62
+ </v-btn>
63
+ </v-col>
64
+ </v-row>
65
+ </v-form>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, reactive, onMounted, onErrorCaptured } from 'vue';
70
+ import { useVendureMutation } from '../../composables/useVendureMutation';
71
+ import setOrderShippingAddressMutation from '#graphql/app/commerce/mutations/setOrderShippingAddress.gql';
72
+ import getCountryListQuery from '#graphql/app/commerce/queries/getCountryList.gql';
73
+ import { useVendureQuery } from '../../composables/useVendureQuery';
74
+ import { useNotification } from '~//composables/useNotifications';
75
+
76
+ const emit = defineEmits(['address-saved', 'address-error', 'form-reset']);
77
+ const { showNotification } = useNotification();
78
+ const loading = ref(false);
79
+ const error = ref<string | null>(null);
80
+ const useAsShippingAddress = ref(false);
81
+
82
+ const errors = reactive({
83
+ fullName: '',
84
+ streetLine1: '',
85
+ streetLine2: '',
86
+ city: '',
87
+ province: '',
88
+ postalCode: '',
89
+ phoneNumber: '',
90
+ countryCode: '',
91
+ });
92
+
93
+ const { data: countriesResult } = useVendureQuery(getCountryListQuery);
94
+ type Province = { code: string; name: string };
95
+ type Country = { code: string; name: string; provinces?: Province[] };
96
+
97
+ const countries = ref<Country[]>([]);
98
+ const regions = ref<Province[]>([]);
99
+
100
+ const shippingAddress = reactive({
101
+ fullName: '',
102
+ streetLine1: '',
103
+ streetLine2: '',
104
+ city: '',
105
+ province: null,
106
+ postalCode: '',
107
+ phoneNumber: '',
108
+ countryCode: null,
109
+ });
110
+
111
+ const { mutate: setShippingAddress } = useVendureMutation(setOrderShippingAddressMutation);
112
+
113
+ const updateRegions = (countryCode: string | null) => {
114
+ const country = countries.value.find((c: any) => c.code === countryCode);
115
+ regions.value = country?.provinces || [];
116
+ };
117
+
118
+ const handleReset = () => {
119
+ Object.assign(shippingAddress, {
120
+ fullName: '',
121
+ streetLine1: '',
122
+ streetLine2: '',
123
+ city: '',
124
+ province: '',
125
+ postalCode: '',
126
+ phoneNumber: '',
127
+ countryCode: '',
128
+ });
129
+ useAsShippingAddress.value = false;
130
+ emit('form-reset');
131
+ };
132
+
133
+ const setShippingAddressHandler = async () => {
134
+ loading.value = true;
135
+ error.value = null;
136
+ try {
137
+ const result = await setShippingAddress({
138
+ input: {
139
+ fullName: shippingAddress.fullName,
140
+ streetLine1: shippingAddress.streetLine1,
141
+ streetLine2: shippingAddress.streetLine2,
142
+ city: shippingAddress.city,
143
+ province: shippingAddress.province,
144
+ postalCode: shippingAddress.postalCode,
145
+ phoneNumber: shippingAddress.phoneNumber,
146
+ countryCode: shippingAddress.countryCode,
147
+ },
148
+ });
149
+ showNotification({ type: 'success', message: 'Shipping address updated' });
150
+ emit('address-saved', result?.setOrderShippingAddress?.shippingAddress);
151
+ } catch (err) {
152
+ error.value = err instanceof Error ? err.message : 'Failed to update shipping address';
153
+ showNotification({ type: 'error', message: error.value });
154
+ emit('address-error', err instanceof Error ? err : new Error(error.value));
155
+ } finally {
156
+ loading.value = false;
157
+ }
158
+ };
159
+
160
+ onMounted(() => {
161
+ if (countriesResult.value) {
162
+ countries.value = countriesResult.value.countries;
163
+ }
164
+ });
165
+ onErrorCaptured((err) => {
166
+ error.value = err instanceof Error ? err.message : 'An unexpected error occurred';
167
+ return false;
168
+ });
169
+ </script>
170
+
171
+ <style scoped>
172
+ .gap-4 {
173
+ gap: 1rem;
174
+ }
175
+ </style>