@viur/shop-components 0.13.1 → 0.13.2-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viur/shop-components",
3
- "version": "0.13.1",
3
+ "version": "0.13.2-1",
4
4
  "description": "Frontend Vue components for the shop module of ViUR",
5
5
  "repository": {
6
6
  "type": "git",
@@ -81,11 +81,18 @@
81
81
  </sl-dialog>
82
82
  </template>
83
83
 
84
+ <template v-if="shopStore.state.order?.['payment_provider'] !== null && shopStore.state.order?.['payment_provider'].startsWith('paypal')">
85
+ <sl-dialog v-if="state.paymentPopup" :label="$t('viur.shop.order_pay')" :open="state.paymentPopup" @sl-after-hide="state.paymentPopup=false">
86
+ <payment-provider-paypal @cancel="paymentCanceled"></payment-provider-paypal>
87
+ </sl-dialog>
88
+ </template>
89
+
84
90
  <slot name="template_confirm">
85
91
  </slot>
86
92
  </template>
87
93
  <script setup>
88
94
  import {computed, onBeforeMount, reactive, watch, useTemplateRef} from 'vue'
95
+ import PaymentProviderPaypal from '../components/PaymentProviderPaypal.vue';
89
96
  import { useViurShopStore } from '../shop';
90
97
  import boneUtils from '@viur/vue-utils/bones/utils'
91
98
  import {Request} from '@viur/vue-utils'
@@ -144,7 +151,7 @@ async function startCheckout(){
144
151
  }
145
152
  }
146
153
  state.paymentPopup=true
147
- shopStore.checkout()
154
+ shopStore.checkoutStart();
148
155
  }
149
156
 
150
157
  //close popup if payment successfull
@@ -0,0 +1,256 @@
1
+ <template>
2
+ <div class="loading-wrapper" v-if="state.loading">
3
+ <sl-spinner class="loading"></sl-spinner>
4
+ </div>
5
+
6
+ <div class="loading-wrapper" v-if="PaymentCheckIsActive">
7
+ <sl-spinner class="loading"></sl-spinner>
8
+ {{ $t('messages.wait_for_payment') }}
9
+ </div>
10
+ <div class="form-wrapper">
11
+ <sl-alert :open="state.hasError" variant="danger">{{ state.errorMessage }}</sl-alert>
12
+
13
+ <div id="paypal-button-container"></div>
14
+
15
+ <sl-button :disabled="state.loading" variant="danger" @click="cancelPayment">
16
+ {{ $t('actions.cancel') }}
17
+ </sl-button>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup>
22
+ import {Request} from '@viur/vue-utils';
23
+ import {HTTPError} from '@viur/vue-utils/utils/request.js';
24
+ import {useIntervalFn} from '@vueuse/core';
25
+ import {onBeforeMount, reactive} from 'vue';
26
+ import {useOrder} from '../composables/order';
27
+ import {useViurShopStore} from '../shop';
28
+
29
+ const shopStore = useViurShopStore();
30
+ const {fetchOrder} = useOrder();
31
+
32
+ const emits = defineEmits(['cancel']);
33
+
34
+ // TODO: Duplicate code
35
+ const {pause: PaymentCheckPause, resume: PaymentCheckResume, isActive: PaymentCheckIsActive} = useIntervalFn(() => {
36
+ console.debug('checking ...');
37
+
38
+ fetchOrder(shopStore.state.orderKey).then(() => {
39
+ if (shopStore.state.order?.['is_paid']) {
40
+ console.debug('Order is paid');
41
+ shopStore.navigateToTab('complete');
42
+ PaymentCheckPause();
43
+ }
44
+ });
45
+
46
+ }, 2000, {immediate: false});
47
+
48
+
49
+ const state = reactive({
50
+ paymentHandler: {},
51
+ loading: false,
52
+ hasError: false,
53
+ errorMessage: null,
54
+ waitPayment: false,
55
+ order_id: null,
56
+ debugPaypal: false,
57
+ });
58
+
59
+ /**
60
+ * Initialize the payment type creation with Unzer-UI components
61
+ */
62
+ function initPaypalForm() {
63
+ if (shopStore.state.order?.['payment_provider'] !== 'paypal_checkout') {
64
+ console.error(`PayPal Checkout does not work with ${shopStore.state.order?.['payment_provider']}.`);
65
+ return null;
66
+ }
67
+
68
+ const scriptConfig = {
69
+ // https://developer.paypal.com/sdk/js/configuration/#configure-and-customize-your-integration
70
+ 'client-id': shopStore.state.paymentProviderData.public_key,
71
+ 'buyer-country': shopStore.state.order.billing_address.dest['country'].toUpperCase() ?? 'DE',
72
+ 'currency': 'EUR',
73
+ 'components': 'buttons,applepay',
74
+ 'enable-funding': 'venmo,paylater,card,sofort,sepa,giropay',
75
+ 'debug': state.debugPaypal,
76
+ };
77
+
78
+ const paypalScriptPromise = new Promise((resolve, reject) => {
79
+ const paypalScript = document.createElement('script');
80
+ paypalScript.setAttribute('src', `https://www.paypal.com/sdk/js?${new URLSearchParams(scriptConfig).toString()}`);
81
+ paypalScript.onload = () => {
82
+ resolve(paypalScript);
83
+ };
84
+ paypalScript.onerror = reject;
85
+ document.head.appendChild(paypalScript);
86
+ });
87
+
88
+ paypalScriptPromise.then(() => {
89
+ console.debug('paypalScriptPromise resolved - READY');
90
+ console.debug(shopStore.state.paymentProviderData);
91
+
92
+ const paypalButtons = window.paypal.Buttons({
93
+ style: {
94
+ shape: 'rect',
95
+ layout: 'vertical',
96
+ color: 'gold',
97
+ label: 'paypal',
98
+ },
99
+ message: {
100
+ amount: 100,
101
+ },
102
+
103
+ async createOrder() {
104
+ console.debug('createOrder');
105
+ state.loading = true;
106
+
107
+ if (shopStore.state.order?.['is_ordered'] && shopStore.state.order?.['is_checkout_in_progress'] && !shopStore.state.order?.['is_paid']) {
108
+ // already ordered; needs a re-checkout method to create a new order. But we can try it again with the last order_id
109
+ state.order_id = (shopStore.state.order?.payment?.payments ?? []).at(-1)?.payment_id;
110
+ return state.order_id;
111
+ }
112
+
113
+ try {
114
+ const resp = await shopStore.checkoutOrder(); // must be inline awaited to return order_id on success
115
+ // TODO: re-checkout with new attempt or different method
116
+ console.debug('checkoutOrder', resp);
117
+ const orderData = resp.payment;
118
+
119
+ if (orderData.id) {
120
+ // SUCCESS; return the order_id to the PayPal instance
121
+ state.order_id = orderData.id;
122
+ return orderData.id;
123
+ } else {
124
+ // ERROR; show error message
125
+ const errorDetail = orderData?.details?.[0];
126
+ const errorMessage = errorDetail
127
+ ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
128
+ : JSON.stringify(orderData);
129
+ console.error(errorDetail);
130
+ paymentError(`Could not initiate PayPal Checkout...<br><br>${errorMessage}`);
131
+ }
132
+ } catch (error) {
133
+ paymentError(error);
134
+ return null;
135
+ } finally {
136
+ state.loading = false;
137
+ }
138
+ },
139
+
140
+ /**
141
+ * Buyer approved the payment, send it to the backend to capture it and mark as paid.
142
+ */
143
+ async onApprove(data, actions) {
144
+ console.debug('onApprove', data, actions);
145
+ state.loading = true;
146
+
147
+ const paymenttarget = shopStore.state.order?.['payment_provider'].replace(/-/g, '_');
148
+ Request.post(`${shopStore.state.shopUrl}/pp_${paymenttarget}/capture_order`, {
149
+ dataObj: {
150
+ order_key: shopStore.state.orderKey,
151
+ order_id: state.order_id,
152
+ },
153
+ })
154
+ .then(async (resp) => {
155
+ console.debug('capture_order', resp);
156
+ const data = await resp.json();
157
+ const orderData = data.payment;
158
+
159
+ // Three cases to handle:
160
+ // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
161
+ // (2) Other non-recoverable errors -> Show a failure message
162
+ // (3) Successful transaction -> Show confirmation or thank you message
163
+ const errorDetail = orderData?.details?.[0];
164
+
165
+ if (errorDetail?.issue === 'INSTRUMENT_DECLINED') {
166
+ // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
167
+ // recoverable state, per
168
+ // https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
169
+ return actions.restart();
170
+ } else if (errorDetail) {
171
+ // (2) Other non-recoverable errors -> Show a failure message
172
+ paymentError(`${errorDetail.description} (${orderData?.debug_id})`);
173
+ } else if (!orderData.purchase_units) {
174
+ paymentError(`Missing purchase_units`);
175
+ } else {
176
+ // (3) Successful transaction -> Show confirmation or thank you message
177
+ // Or go to another URL: actions.redirect('thank_you.html');
178
+ state.loading = false;
179
+ state.hasError = false;
180
+ state.waitPayment = true;
181
+ PaymentCheckResume();
182
+ }
183
+ })
184
+ .catch(paymentError);
185
+ },
186
+
187
+ onError: paymentError,
188
+
189
+ // appSwitchWhenAvailable: true,
190
+ });
191
+ paypalButtons.render('#paypal-button-container');
192
+ });
193
+
194
+ state.loading = false;
195
+ }
196
+
197
+
198
+ /**
199
+ * Handle an error
200
+ *
201
+ * @param error The error. In best cases an object from unzer
202
+ * with a translated (and detailed) description for the customer
203
+ */
204
+ function paymentError(error) {
205
+ console.error(error);
206
+ state.loading = false;
207
+ state.hasError = true;
208
+ if (error && error.constructor.name === HTTPError.name && error.response.headers.get('x-viur-shop-error')) {
209
+ error.response.json().then(res => {
210
+ console.error(res.errors);
211
+ state.errorMessage = res.errors.map(err => err.customer_message || err.message).join(', ');
212
+ });
213
+ } else {
214
+ state.errorMessage = error.customerMessage || error.message || error || 'Error';
215
+ }
216
+ }
217
+
218
+ function cancelPayment() {
219
+ PaymentCheckPause();
220
+ emits('cancel');
221
+ }
222
+
223
+ onBeforeMount(() => {
224
+ console.debug('mounting', shopStore.state.paymentProviderData);
225
+ state.loading = true;
226
+ if (!shopStore.state.paymentProviderData) {
227
+ shopStore.checkoutStart().then(() => {
228
+ initPaypalForm();
229
+ fetchOrder(shopStore.state.orderKey); // refresh order after checkout_start freeze
230
+ }).catch((error) => {
231
+ console.log(error);
232
+ });
233
+ } else {
234
+ initPaypalForm();
235
+ }
236
+ });
237
+
238
+ </script>
239
+
240
+ <style scoped>
241
+ .loading-wrapper {
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ }
246
+
247
+ .form-wrapper {
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 20px;
251
+ }
252
+
253
+ .loading {
254
+ font-size: 3rem;
255
+ }
256
+ </style>
@@ -93,6 +93,7 @@ const {saveBirthdate} = useAddress();
93
93
 
94
94
  const emits = defineEmits(['cancel']);
95
95
 
96
+ // TODO: Duplicate code
96
97
  const {pause: PaymentCheckPause, resume: PaymentCheckResume, isActive: PaymentCheckIsActive} = useIntervalFn(() => {
97
98
  console.debug('checking ...');
98
99
 
@@ -339,7 +340,7 @@ function cancelPayment() {
339
340
  onBeforeMount(() => {
340
341
  state.loading = true;
341
342
  if (!shopStore.state.paymentProviderData) {
342
- shopStore.checkout().then(() => {
343
+ shopStore.checkoutStart().then(() => {
343
344
  initUnzerForm();
344
345
  fetchOrder(shopStore.state.orderKey); // refresh order after checkout_start freeze
345
346
  }).catch((error) => {
package/src/shop.js CHANGED
@@ -212,7 +212,7 @@ export const useViurShopStore = defineStore("viurshopStore", () => {
212
212
  fetchPaymentMeta()
213
213
  }
214
214
 
215
- function checkout(){
215
+ function checkoutStart(){
216
216
  //request Payment
217
217
  return new Promise((resolve,reject)=>{
218
218
  Request.post(`${state.shopUrl}/order/checkout_start`,{dataObj:{
@@ -300,7 +300,7 @@ export const useViurShopStore = defineStore("viurshopStore", () => {
300
300
  navigateToPrevious,
301
301
  tabValidation,
302
302
  fetchMetaData,
303
- checkout,
303
+ checkoutStart,
304
304
  checkoutOrder,
305
305
  addTab,
306
306
  removeTab