@salla.sa/applepay 3.0.0-alpha.0 → 3.0.0-beta.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/deploy.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://cdn.assets.salla.network/json-schemas/deploy.json",
3
+ "type": "package",
4
+ "publish": {
5
+ "npm": true,
6
+ "cdn": false
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "name": "@salla.sa/applepay",
3
- "version": "3.0.0-alpha.0",
3
+ "version": "3.0.0-beta.1",
4
4
  "description": "Salla Apple Pay light",
5
5
  "main": "dist/app.js",
6
- "scripts": {
7
- "watch": "echo \"There is no watch!\"",
8
- "build": "echo \"There is no build!\"",
9
- "test": "echo \"Error: no test specified\" && exit 1"
10
- },
11
6
  "repository": {
12
7
  "type": "git",
13
8
  "url": "git+https://github.com/SallaApp/twilight.git"
@@ -24,7 +19,11 @@
24
19
  "license": "UNLICENSED",
25
20
  "homepage": "https://github.com/SallaApp/twilight#readme",
26
21
  "dependencies": {
27
- "axios": "^0.23.0"
22
+ "axios": "^1.10.0"
28
23
  },
29
- "gitHead": "741e5aadebda0f980c7c3ed2e9a85036e50d154c"
30
- }
24
+ "scripts": {
25
+ "watch": "echo \"There is no watch!\"",
26
+ "build": "echo \"There is no build!\"",
27
+ "test": "echo \"Error: no test specified\" && exit 1"
28
+ }
29
+ }
package/src/DetectOS.js CHANGED
@@ -1,75 +1,84 @@
1
1
  export default {
2
- options: [],
3
- header: [navigator.platform, navigator.userAgent, navigator.appVersion, navigator.vendor, window.opera],
4
- dataos: [
5
- {name: 'Windows Phone', value: 'Windows Phone', version: 'OS'},
6
- {name: 'Windows', value: 'Win', version: 'NT'},
7
- {name: 'iPhone', value: 'iPhone', version: 'OS'},
8
- {name: 'iPad', value: 'iPad', version: 'OS'},
9
- {name: 'Kindle', value: 'Silk', version: 'Silk'},
10
- {name: 'Android', value: 'Android', version: 'Android'},
11
- {name: 'PlayBook', value: 'PlayBook', version: 'OS'},
12
- {name: 'BlackBerry', value: 'BlackBerry', version: '/'},
13
- {name: 'Macintosh', value: 'Mac', version: 'OS X'},
14
- {name: 'Linux', value: 'Linux', version: 'rv'},
15
- {name: 'Palm', value: 'Palm', version: 'PalmOS'}
16
- ],
17
- databrowser: [
18
- {name: 'Chrome', value: 'Chrome', version: 'Chrome'},
19
- {name: 'Firefox', value: 'Firefox', version: 'Firefox'},
20
- {name: 'Safari', value: 'Safari', version: 'Version'},
21
- {name: 'Internet Explorer', value: 'MSIE', version: 'MSIE'},
22
- {name: 'Opera', value: 'Opera', version: 'Opera'},
23
- {name: 'BlackBerry', value: 'CLDC', version: 'CLDC'},
24
- {name: 'Mozilla', value: 'Mozilla', version: 'Mozilla'}
25
- ],
26
- init: function () {
27
- var agent = this.header.join(' '),
28
- os = this.matchItem(agent, this.dataos),
29
- browser = this.matchItem(agent, this.databrowser);
2
+ options: [],
3
+ header:
4
+ typeof navigator !== "undefined" && typeof window !== "undefined"
5
+ ? [
6
+ navigator.platform,
7
+ navigator.userAgent,
8
+ navigator.appVersion,
9
+ navigator.vendor,
10
+ window.opera,
11
+ ]
12
+ : [],
13
+ dataos: [
14
+ { name: "Windows Phone", value: "Windows Phone", version: "OS" },
15
+ { name: "Windows", value: "Win", version: "NT" },
16
+ { name: "iPhone", value: "iPhone", version: "OS" },
17
+ { name: "iPad", value: "iPad", version: "OS" },
18
+ { name: "Kindle", value: "Silk", version: "Silk" },
19
+ { name: "Android", value: "Android", version: "Android" },
20
+ { name: "PlayBook", value: "PlayBook", version: "OS" },
21
+ { name: "BlackBerry", value: "BlackBerry", version: "/" },
22
+ { name: "Macintosh", value: "Mac", version: "OS X" },
23
+ { name: "Linux", value: "Linux", version: "rv" },
24
+ { name: "Palm", value: "Palm", version: "PalmOS" },
25
+ ],
26
+ databrowser: [
27
+ { name: "Chrome", value: "Chrome", version: "Chrome" },
28
+ { name: "Firefox", value: "Firefox", version: "Firefox" },
29
+ { name: "Safari", value: "Safari", version: "Version" },
30
+ { name: "Internet Explorer", value: "MSIE", version: "MSIE" },
31
+ { name: "Opera", value: "Opera", version: "Opera" },
32
+ { name: "BlackBerry", value: "CLDC", version: "CLDC" },
33
+ { name: "Mozilla", value: "Mozilla", version: "Mozilla" },
34
+ ],
35
+ init: function () {
36
+ var agent = this.header.join(" "),
37
+ os = this.matchItem(agent, this.dataos),
38
+ browser = this.matchItem(agent, this.databrowser);
30
39
 
31
- return {os: os, browser: browser};
32
- },
33
- matchItem: function (string, data) {
34
- var i = 0,
35
- j = 0,
36
- html = '',
37
- regex,
38
- regexv,
39
- match,
40
- matches,
41
- version;
40
+ return { os: os, browser: browser };
41
+ },
42
+ matchItem: function (string, data) {
43
+ var i = 0,
44
+ j = 0,
45
+ html = "",
46
+ regex,
47
+ regexv,
48
+ match,
49
+ matches,
50
+ version;
42
51
 
43
- for (i = 0; i < data.length; i += 1) {
44
- regex = new RegExp(data[i].value, 'i');
45
- match = regex.test(string);
46
- if (match) {
47
- regexv = new RegExp(data[i].version + '[- /:;]([\\d._]+)', 'i');
48
- matches = string.match(regexv);
49
- version = '';
50
- if (matches) {
51
- if (matches[1]) {
52
- matches = matches[1];
53
- }
54
- }
55
- if (matches) {
56
- matches = matches.split(/[._]+/);
57
- for (j = 0; j < matches.length; j += 1) {
58
- if (j === 0) {
59
- version += matches[j] + '.';
60
- } else {
61
- version += matches[j];
62
- }
63
- }
64
- } else {
65
- version = '0';
66
- }
67
- return {
68
- name: data[i].name,
69
- version: parseFloat(version)
70
- };
52
+ for (i = 0; i < data.length; i += 1) {
53
+ regex = new RegExp(data[i].value, "i");
54
+ match = regex.test(string);
55
+ if (match) {
56
+ regexv = new RegExp(data[i].version + "[- /:;]([\\d._]+)", "i");
57
+ matches = string.match(regexv);
58
+ version = "";
59
+ if (matches) {
60
+ if (matches[1]) {
61
+ matches = matches[1];
62
+ }
63
+ }
64
+ if (matches) {
65
+ matches = matches.split(/[._]+/);
66
+ for (j = 0; j < matches.length; j += 1) {
67
+ if (j === 0) {
68
+ version += matches[j] + ".";
69
+ } else {
70
+ version += matches[j];
71
71
  }
72
+ }
73
+ } else {
74
+ version = "0";
72
75
  }
73
- return {name: 'unknown', version: 0};
76
+ return {
77
+ name: data[i].name,
78
+ version: parseFloat(version),
79
+ };
80
+ }
74
81
  }
82
+ return { name: "unknown", version: 0 };
83
+ },
75
84
  };
package/src/index.js CHANGED
@@ -1,9 +1,12 @@
1
- import Http from './http';
1
+ import http from './http';
2
2
  import DetectOS from './DetectOS';
3
- import { mutateShipmentAddress } from './utils';
3
+ import { mutateShippingContact } from './utils';
4
4
 
5
- window.Salla = window.Salla || {};
6
- window.Salla.Payments = window.Salla.Payments || {};
5
+ // SSR-safe: only access window in browser environment
6
+ if (typeof window !== 'undefined') {
7
+ window.Salla = window.Salla || {};
8
+ window.Salla.Payments = window.Salla.Payments || {};
9
+ }
7
10
 
8
11
  /**
9
12
  * Full Example
@@ -35,7 +38,9 @@ window.Salla.Payments = window.Salla.Payments || {};
35
38
  * // }
36
39
  * });
37
40
  */
38
- window.SallaApplePay = {
41
+
42
+ // SSR-safe: define SallaApplePay as a stub during SSR, actual object in browser
43
+ const SallaApplePay = typeof window !== 'undefined' ? {
39
44
  session: null,
40
45
  detail: null,
41
46
  address_id: null,
@@ -43,6 +48,9 @@ window.SallaApplePay = {
43
48
  total: undefined,
44
49
  request: undefined,
45
50
  id: undefined,
51
+ countryCode: null,
52
+ totals: [],
53
+ shippingCompany: null,
46
54
  init: function () {
47
55
  document.removeEventListener('payments::apple-pay.start-transaction', SallaApplePay.startSession);
48
56
  Salla.event.addEventListener('payments::apple-pay.start-transaction', SallaApplePay.startSession);
@@ -66,7 +74,7 @@ window.SallaApplePay = {
66
74
  if (!SallaApplePay.detail.validateMerchant.onFailed) {
67
75
  SallaApplePay.detail.validateMerchant.onFailed = (response) => {
68
76
  salla.logger.log(JSON.stringify(response))
69
- salla.api.handleErrorResponse(response);
77
+ salla.api.handleErrorResponse({response});
70
78
  SallaApplePay.onCancel({}, response.data.error.message);
71
79
  };
72
80
  }
@@ -80,11 +88,11 @@ window.SallaApplePay = {
80
88
  },
81
89
 
82
90
  prepareLineItems: function () {
83
- if(!SallaApplePay.detail?.items?.length){
91
+ if (!SallaApplePay.detail?.items?.length) {
84
92
  SallaApplePay.detail.items = [
85
93
  {
86
94
  label: salla.lang.get('pages.cart.items_total'),
87
- amount: SallaApplePay.detail.amount
95
+ amount: parseFloat(SallaApplePay.detail.amount).toString()
88
96
  }
89
97
  ]
90
98
  }
@@ -93,12 +101,16 @@ window.SallaApplePay = {
93
101
  },
94
102
 
95
103
  prepareTotal: function () {
96
- return {
97
- // apple ask to use business name
98
- label: window.location.hostname || 'Salla',
99
- //label: salla.lang.get('pages.cart.final_total'),
100
- amount: SallaApplePay.detail.amount
101
- }
104
+ return {
105
+ // apple ask to use business name
106
+ label: window.location.hostname || 'Salla',
107
+ //label: salla.lang.get('pages.cart.final_total'),
108
+ amount: parseFloat(SallaApplePay.detail.amount).toString()
109
+ }
110
+ },
111
+
112
+ isPhysical() {
113
+ return SallaApplePay.detail?.requiredShippingContactFields?.includes('postalAddress');
102
114
  },
103
115
 
104
116
  startSession: async function (event) {
@@ -117,7 +129,7 @@ window.SallaApplePay = {
117
129
  }
118
130
 
119
131
  SallaApplePay.request = {
120
- countryCode: 'SA',
132
+ countryCode: SallaApplePay.detail.countryCode || 'SA',
121
133
  supportsCouponCode: true,
122
134
  couponCode: '',
123
135
  currencyCode: SallaApplePay.detail.currency || 'SAR',
@@ -141,30 +153,70 @@ window.SallaApplePay = {
141
153
  SallaApplePay.session.onvalidatemerchant = SallaApplePay.onValidateMerchant;
142
154
  SallaApplePay.session.onpaymentauthorized = SallaApplePay.onPaymentAuthorized;
143
155
  SallaApplePay.session.oncancel = SallaApplePay.onCancel;
144
- SallaApplePay.session.oncouponcodechanged = SallaApplePay.oncouponcodechanged;
145
- SallaApplePay.session.onpaymentmethodselected = SallaApplePay.onpaymentmethodselected;
146
-
147
- SallaApplePay.session.begin();
156
+ SallaApplePay.session.oncouponcodechanged = SallaApplePay.onCouponCodeChanged;
157
+ SallaApplePay.session.onpaymentmethodselected = SallaApplePay.onPaymentMethodSelected;
158
+
159
+ // Hook between session creation and begin(). Return truthy to skip
160
+ // begin() (caller handles it later, e.g. after async work).
161
+ let skipBegin;
162
+ if (typeof SallaApplePay.detail.onStartedSession === 'function') {
163
+ skipBegin = SallaApplePay.detail.onStartedSession(SallaApplePay.session);
164
+ }
165
+ if (!skipBegin) {
166
+ SallaApplePay.session.begin();
167
+ }
148
168
  },
149
- async onpaymentmethodselected(event) {
150
- await SallaApplePay.recalculateTotal();
151
- const updatedPaymentDetails = {
169
+ onPaymentMethodSelected: async (event) => {
170
+ salla.logger.log('🍏 Pay: onPaymentMethodSelected', event);
171
+
172
+ // perform recalculate here only if digital product
173
+ // if physical recalculate performed with shipping company
174
+ if (!SallaApplePay.isPhysical()) {
175
+ try {
176
+ await SallaApplePay.recalculateTotal();
177
+
178
+ const updatedPaymentDetails = {
179
+ newTotal: SallaApplePay.prepareTotal(),
180
+ newLineItems: SallaApplePay.prepareLineItems()
181
+ };
182
+
183
+ salla.logger.log('🍏 Pay: completePaymentMethodSelection', updatedPaymentDetails);
184
+
185
+ SallaApplePay.session.completePaymentMethodSelection(updatedPaymentDetails);
186
+
187
+ } catch (error) {
188
+ salla.logger.warn('🍏 Pay: Failed recalculate total', error);
189
+
190
+ SallaApplePay.session.completePaymentMethodSelection({
191
+ newTotal: SallaApplePay.prepareTotal(),
192
+ newLineItems: SallaApplePay.prepareLineItems(),
193
+ status: SallaApplePay.session.STATUS_FAILURE,
194
+ errors: [new window.ApplePayError("unknown", undefined, error?.response?.data?.error?.message || error?.response?.data?.error?.code || 'Failed to recalculate total')]
195
+ });
196
+ }
197
+ return;
198
+ }
199
+
200
+ salla.logger.log('🍏 Pay: completePaymentMethodSelection', {
152
201
  newTotal: SallaApplePay.prepareTotal(),
153
202
  newLineItems: SallaApplePay.prepareLineItems(),
154
- };
203
+ });
155
204
 
156
- SallaApplePay.session.completePaymentMethodSelection(updatedPaymentDetails);
205
+ SallaApplePay.session.completePaymentMethodSelection({
206
+ newTotal: SallaApplePay.prepareTotal(),
207
+ newLineItems: SallaApplePay.prepareLineItems(),
208
+ });
157
209
  },
158
210
 
159
- oncouponcodechanged(event) {
211
+ onCouponCodeChanged(event) {
160
212
  Salla.event.dispatch('payments::apple-pay.coupon.change', event);
161
213
 
162
- return Http.post(SallaApplePay.detail.oncouponcodechanged.url.replace('{id}', SallaApplePay.id), {
214
+ return http.post(SallaApplePay.detail.onCouponCodeChanged.url.replace('{id}', SallaApplePay.id), {
163
215
  'coupon': event.couponCode,
164
216
  'payment_method': 'apple_pay',
165
- }, async ({data}) => {
166
- if (typeof SallaApplePay.detail.oncouponcodechanged.onSuccess === 'function') {
167
- SallaApplePay.detail.oncouponcodechanged.onSuccess(data);
217
+ }, async ({ data }) => {
218
+ if (typeof SallaApplePay.detail.onCouponCodeChanged.onSuccess === 'function') {
219
+ SallaApplePay.detail.onCouponCodeChanged.onSuccess(data);
168
220
  }
169
221
 
170
222
  salla.log('🍏 Pay: Coupon applied success');
@@ -181,8 +233,8 @@ window.SallaApplePay = {
181
233
  Salla.event.dispatch('payments::apple-pay.coupon.failed', response);
182
234
 
183
235
  // SallaApplePay.abortSession();
184
- if (typeof SallaApplePay.detail.oncouponcodechanged.onFailed === 'function') {
185
- SallaApplePay.detail.oncouponcodechanged.onFailed(response);
236
+ if (typeof SallaApplePay.detail.onCouponCodeChanged.onFailed === 'function') {
237
+ SallaApplePay.detail.onCouponCodeChanged.onFailed(response);
186
238
  }
187
239
 
188
240
  await SallaApplePay.recalculateTotal();
@@ -208,35 +260,80 @@ window.SallaApplePay = {
208
260
  onPaymentAuthorized: async (event) => {
209
261
  salla.logger.log('🍏 Pay: onPaymentAuthorized', event.payment);
210
262
 
211
- // Update the payment address
212
- await mutateShipmentAddress(SallaApplePay, event.payment.shippingContact, true);
263
+ // update guest details
264
+ try {
265
+ await mutateShippingContact(SallaApplePay, event.payment.shippingContact, true);
266
+ } catch (error) {
267
+ salla.logger.error('🍏 Pay: Failed to update guest contact details', error);
268
+
269
+ const response = error?.response || error;
270
+
271
+ // Parse backend errors for shipping contact fields
272
+ const fields = response?.data?.error?.fields || {};
273
+ const errors = [];
274
+
275
+ // Map backend field errors to Apple Pay contact field errors
276
+ if (fields?.email && fields.email.length > 0) {
277
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'emailAddress', fields.email[0]));
278
+ }
279
+
280
+ if (fields?.phone && fields.phone.length > 0) {
281
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'phoneNumber', fields.phone[0]));
282
+ }
283
+
284
+ if (fields?.first_name && fields.first_name.length > 0) {
285
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'name', fields.first_name[0]));
286
+ } else if (fields?.last_name && fields.last_name.length > 0) {
287
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'name', fields.last_name[0]));
288
+ }
289
+
290
+ // If no specific field errors, use general error message
291
+ if (errors.length === 0) {
292
+ const errorMessage = response?.data?.error?.message || response?.data?.error?.code || 'Invalid shipping contact details';
293
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'name', errorMessage));
294
+ }
295
+
296
+ Salla.event.dispatch('payments::apple-pay.authorized.failed', response);
297
+
298
+ if (typeof SallaApplePay.detail.authorized.onFailed === 'function') {
299
+ SallaApplePay.detail.authorized.onFailed(response);
300
+ }
301
+
302
+ // Complete shipping contact selection with invalid contact status
303
+ SallaApplePay.session.completePayment({
304
+ status: ApplePaySession.STATUS_INVALID_SHIPPING_CONTACT,
305
+ errors: errors
306
+ });
307
+
308
+ return;
309
+ }
213
310
 
214
311
  Salla.event.dispatch('payments::apple-pay.authorized.init', event);
215
- Http.post(SallaApplePay.detail.authorized.url.replace('{id}', SallaApplePay.id), {
312
+ http.post(SallaApplePay.detail.authorized.url.replace('{id}', SallaApplePay.id), {
216
313
  payment_method: 'apple_pay',
217
314
  applepay_token: JSON.stringify(event.payment)
218
- }, ({data}) => {
315
+ }, ({ data }) => {
219
316
  Salla.event.dispatch('payments::apple-pay.authorized.success', data);
220
317
 
221
- SallaApplePay.session.completePayment(ApplePaySession.STATUS_SUCCESS);
222
-
223
318
  if (typeof SallaApplePay.detail.authorized.onSuccess === 'function') {
224
319
  SallaApplePay.detail.authorized.onSuccess(data);
225
320
  }
321
+
322
+ SallaApplePay.session.completePayment(ApplePaySession.STATUS_SUCCESS);
226
323
  }, (error) => {
227
324
 
228
325
  let response = error?.response;
229
326
 
230
327
  Salla.event.dispatch('payments::apple-pay.authorized.failed', response);
231
328
 
232
- SallaApplePay.session.completePayment({
233
- status: ApplePaySession.STATUS_FAILURE,
234
- errors: [new ApplePayError("unknown", undefined, response?.data?.error?.message || response?.data?.error?.code || 'Failed to parse authorized response')]
235
- })
236
-
237
329
  if (typeof SallaApplePay.detail.authorized.onFailed === 'function') {
238
330
  SallaApplePay.detail.authorized.onFailed(response);
239
331
  }
332
+
333
+ SallaApplePay.session.completePayment({
334
+ status: ApplePaySession.STATUS_FAILURE,
335
+ errors: [new window.ApplePayError("unknown", undefined, response?.data?.error?.message || response?.data?.error?.code || 'Failed to parse authorized response')]
336
+ })
240
337
  });
241
338
  },
242
339
 
@@ -251,7 +348,7 @@ window.SallaApplePay = {
251
348
  Salla.event.dispatch('payments::apple-pay.validate-merchant.init', event);
252
349
 
253
350
  // Post request to validate merchant
254
- const { data } = await Http.post(SallaApplePay.detail.validateMerchant.url.replace('{id}', SallaApplePay.id), {
351
+ const { data } = await http.post(SallaApplePay.detail.validateMerchant.url.replace('{id}', SallaApplePay.id), {
255
352
  validation_url: event.validationURL
256
353
  });
257
354
 
@@ -300,7 +397,7 @@ window.SallaApplePay = {
300
397
  salla.logger.log('🍏 Pay: onShippingContactSelected', event.shippingContact);
301
398
 
302
399
  // create address for shipping calculation
303
- mutateShipmentAddress(SallaApplePay, event.shippingContact);
400
+ await mutateShippingContact(SallaApplePay, event.shippingContact);
304
401
  },
305
402
 
306
403
  /**
@@ -310,25 +407,54 @@ window.SallaApplePay = {
310
407
  *
311
408
  */
312
409
  onShippingMethodSelected: async (event) => {
313
- salla.logger.log(event);
314
410
 
315
411
  let shipping_ids = event.shippingMethod.identifier.split(',');
412
+ let shippingMethod = {
413
+ ship_id: shipping_ids[0],
414
+ private_ship_id: typeof shipping_ids[1] === 'undefined' ? null : shipping_ids[1],
415
+ type: typeof shipping_ids[2] === 'undefined' ? null : shipping_ids[2],
416
+ route_id: typeof shipping_ids[3] === 'undefined' ? null : shipping_ids[3],
417
+ };
316
418
 
317
- try {
318
- await SallaApplePay.selectApplePayShippingMethod(shipping_ids[0], typeof shipping_ids[1] === 'undefined' ? null : shipping_ids[1]);
419
+ salla.logger.log('🍏 Pay: onShippingMethodSelected', {
420
+ event,
421
+ previous: SallaApplePay.shippingCompany,
422
+ current: shippingMethod,
423
+ });
319
424
 
320
- await SallaApplePay.recalculateTotal();
425
+ if (SallaApplePay.shouldUpdateShippingCompany(shippingMethod)) {
426
+ try {
427
+
428
+ await SallaApplePay.selectApplePayShippingMethod(shippingMethod);
429
+
430
+ await SallaApplePay.recalculateTotal();
431
+
432
+ } catch (error) {
433
+ salla.logger.warn('🍏 Pay: Failed set the shipping details to api', error);
434
+
435
+ // todo :: find a better handling for error without abort session
436
+ SallaApplePay.session.completeShippingMethodSelection({
437
+ newTotal: SallaApplePay.prepareTotal(),
438
+ newLineItems: SallaApplePay.prepareLineItems(),
439
+ status: SallaApplePay.session.STATUS_INVALID_SHIPPING_POSTAL_ADDRESS,
440
+ errors: [
441
+ new window.ApplePayError('addressUnserviceable')
442
+ ]
443
+ });
444
+
445
+ return;
446
+ }
447
+ }
321
448
 
322
- SallaApplePay.session.completeShippingMethodSelection({
323
- newTotal: SallaApplePay.prepareTotal(),
324
- newLineItems: SallaApplePay.prepareLineItems(),
325
- });
326
- } catch (error) {
327
- salla.logger.warn('🍏 Pay: Failed set the shipping details to api', error);
449
+ salla.logger.log('🍏 Pay: completeShippingMethodSelection', {
450
+ newTotal: SallaApplePay.prepareTotal(),
451
+ newLineItems: SallaApplePay.prepareLineItems(),
452
+ });
328
453
 
329
- // todo :: find a better handling for error without abort session
330
- SallaApplePay.abortSession();
331
- }
454
+ SallaApplePay.session.completeShippingMethodSelection({
455
+ newTotal: SallaApplePay.prepareTotal(),
456
+ newLineItems: SallaApplePay.prepareLineItems(),
457
+ });
332
458
  },
333
459
 
334
460
 
@@ -338,6 +464,10 @@ window.SallaApplePay = {
338
464
  }
339
465
  },
340
466
 
467
+ shouldUpdateShippingCompany(shippingCompany) {
468
+ return SallaApplePay.shippingCompany?.ship_id != shippingCompany?.ship_id || SallaApplePay.shippingCompany?.private_ship_id != shippingCompany?.private_ship_id
469
+ },
470
+
341
471
  getApplePaySessionVersion: () => {
342
472
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;
343
473
 
@@ -363,24 +493,32 @@ window.SallaApplePay = {
363
493
  },
364
494
 
365
495
  recalculateTotal: () => {
366
- salla.logger.log('Recalculate Total');
496
+ salla.logger.log('🍏 Pay: recalculate total');
367
497
 
368
- return Http.requestWithSupportAjax(SallaApplePay.detail.recalculateTotal.url.replace('{id}', SallaApplePay.id),{}, 'get').then((data) => {
498
+ return http.get(SallaApplePay.detail.recalculateTotal.url.replace('{id}', SallaApplePay.id), ({ data }) => {
499
+ salla.logger.log('🍏 Pay: recalculate total success', data);
369
500
  let cart = data.data.initial_data?.cart || data.data.cart;
501
+ let payments = data.data.initial_data?.payments || data.data.payments;
502
+
503
+ salla.logger.log('🍏 Pay: recalculate total success', {
504
+ cart,
505
+ payments
506
+ });
507
+
370
508
  // todo :: enhance response from backend
371
- SallaApplePay.detail.amount = cart.total;
509
+ SallaApplePay.detail.amount = payments?.gateways?.applePay?.supportedMultiCurrency ? cart.total_in_customer_currency : cart.total;
372
510
  SallaApplePay.detail.items = (cart.totals || cart.items).map((item) => {
373
511
  return {
374
512
  label: item.title,
375
- amount: item.amount === 'مجاني' ? 0 : item.amount.toString().replace('+', ''),
513
+ amount: (item.amount === 'مجاني' || item.amount === 'Free') ? 0 : item.amount.toString().replace('+', ''),
376
514
  };
377
515
  })
378
516
 
379
517
  // lets remove last element (final total)
380
518
  SallaApplePay.detail.items.pop();
519
+ SallaApplePay.totals = SallaApplePay.detail.items;
381
520
 
382
- return data;
383
- }).catch((error) => {
521
+ }, (error) => {
384
522
  salla.logger.warn('🍏 Pay: recalculate total failed', error);
385
523
 
386
524
  // general error
@@ -389,22 +527,26 @@ window.SallaApplePay = {
389
527
  },
390
528
 
391
529
 
392
- selectApplePayShippingMethod: (company_id, private_company_id) => {
393
- salla.logger.log('🍏 Pay: select shipping method ', 'company_id : ' + company_id, 'private_company_id: ' + private_company_id);
530
+ selectApplePayShippingMethod: (shippingMethod) => {
531
+ salla.logger.log('🍏 Pay: select shipping method ', shippingMethod);
532
+ SallaApplePay.shippingCompany = shippingMethod;
394
533
 
395
- return Http.requestWithSupportAjax(SallaApplePay.detail.shippingMethodSelected.url.replace('{id}', SallaApplePay.id), {
534
+ const payload = {
396
535
  address_id: SallaApplePay.address_id,
397
- company_id: company_id,
398
- private_company_id: private_company_id,
536
+ company_id: shippingMethod.ship_id,
537
+ private_company_id: shippingMethod.private_ship_id,
538
+ type: shippingMethod.type || undefined,
539
+ route_id: shippingMethod.route_id || undefined,
399
540
  payment_method: 'apple_pay'
400
- }, 'post').then(() => {
401
- if (typeof SallaApplePay.detail. shippingMethodSelected.onSuccess === 'function') {
541
+ }
542
+ return http.post(SallaApplePay.detail.shippingMethodSelected.url.replace('{id}', SallaApplePay.id), payload, ({ data }) => {
543
+ if (typeof SallaApplePay.detail.shippingMethodSelected.onSuccess === 'function') {
402
544
  SallaApplePay.detail.shippingMethodSelected.onSuccess(data);
403
545
  }
404
546
 
405
547
  // we don't have any data in this request, lets resolve the promise
406
548
  return true;
407
- }).catch((error) => {
549
+ }, (error) => {
408
550
  salla.logger.warn('🍏 Pay: Set shipping method failed', error);
409
551
 
410
552
  if (typeof SallaApplePay.detail.shippingMethodSelected.onFailed === 'function') {
@@ -427,15 +569,22 @@ window.SallaApplePay = {
427
569
  'label': method.shipping_title,
428
570
  'amount': method.enable_free_shipping ? 0 : method.ship_cost,
429
571
  'detail': '',
430
- 'identifier': method.ship_id.toString() + (method.private_ship_id ? ',' + method.private_ship_id.toString() : '')
572
+ 'identifier': method.ship_id.toString() + (method.private_ship_id ? ',' + method.private_ship_id.toString() : '') + (method.type ? ',' + method.type : '') + (method.route_id ? ',' + method.route_id : '')
431
573
  }))
574
+ } : {};
575
+
576
+ // SSR-safe: export and assign to window in browser
577
+ if (typeof window !== 'undefined') {
578
+ window.SallaApplePay = SallaApplePay;
432
579
  }
433
580
 
434
581
  //applePay doesn't allow iframes
435
- // if (window.ApplePaySession && window.self === window.top && ApplePaySession.canMakePayments()) {
436
- if (window.ApplePaySession?.canMakePayments()) {
582
+ // SSR-safe: only initialize in browser
583
+ if (typeof window !== 'undefined' && window.ApplePaySession?.canMakePayments()) {
437
584
  SallaApplePay.init();
438
- } else {
585
+ } else if (typeof document !== 'undefined') {
439
586
  // You can hide the Apple Pay button easy with add data-show-if-apple-pay-supported to element like <div data-show-if-apple-pay-supported>
440
587
  document.querySelectorAll('data-show-if-apple-pay-supported').forEach(element => element.style.display = 'none');
441
588
  }
589
+
590
+ export default SallaApplePay;
package/src/utils.js CHANGED
@@ -26,10 +26,55 @@ import http from "./http";
26
26
  * @param {ApplePayPaymentContact} shippingContact
27
27
  *
28
28
  */
29
- export function mutateShipmentAddress(SallaApplePay, shippingContact, isAuthorized = false) {
30
- console.log('mutateShipmentAddress called', shippingContact, isAuthorized);
29
+ export async function mutateShippingContact(SallaApplePay, shippingContact, isAuthorized = false) {
30
+ salla.logger.log('🍏 Pay: mutateShippingContact called', shippingContact, isAuthorized);
31
31
 
32
- if (!SallaApplePay.detail.requiredShippingContactFields) {
32
+ if (!SallaApplePay.detail.requiredShippingContactFields || SallaApplePay.detail.requiredShippingContactFields.length == 0) {
33
+ return;
34
+ }
35
+
36
+ if (isAuthorized) {
37
+ if (isGuestCheckout()) {
38
+
39
+ if (
40
+ !shippingContact.emailAddress ||
41
+ !shippingContact.givenName ||
42
+ !shippingContact.familyName ||
43
+ !shippingContact.phoneNumber
44
+ ) {
45
+
46
+ salla.logger.warn('🍏 Pay: Guest contact fields are required', shippingContact);
47
+
48
+ const errors = [];
49
+ if (!shippingContact.emailAddress) {
50
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'emailAddress', 'Email address is required'));
51
+ }
52
+
53
+ if (!shippingContact.phoneNumber) {
54
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'phoneNumber', 'Phone number is required'));
55
+ }
56
+
57
+ if (!shippingContact.givenName || !shippingContact.familyName) {
58
+ errors.push(new window.ApplePayError('shippingContactInvalid', 'name', 'Name is required'));
59
+ }
60
+
61
+ SallaApplePay.session.completePayment({
62
+ status: SallaApplePay.session.STATUS_INVALID_SHIPPING_CONTACT,
63
+ errors: errors
64
+ });
65
+
66
+ return;
67
+ }
68
+
69
+ await updateGuestContact(SallaApplePay, shippingContact);
70
+
71
+ }
72
+
73
+ // if authorized and not guest checkout, do nothing address already added
74
+ return;
75
+ }
76
+
77
+ if (!SallaApplePay.detail.requiredShippingContactFields.includes('postalAddress')) {
33
78
  return;
34
79
  }
35
80
 
@@ -38,15 +83,13 @@ export function mutateShipmentAddress(SallaApplePay, shippingContact, isAuthoriz
38
83
  {
39
84
  'country': shippingContact.country,
40
85
  'city': shippingContact.locality,
41
- 'local': shippingContact.subLocality,
86
+ 'local': shippingContact.subLocality || shippingContact.administrativeArea || shippingContact.locality,
42
87
  'description': shippingContact.subAdministrativeArea,
43
88
  'street': shippingContact.addressLines?.join(", ") || shippingContact.administrativeArea,
44
89
  'country_code': shippingContact.countryCode,
45
90
  'postal_code': shippingContact.postalCode,
46
- 'is_authorized': isAuthorized
47
91
  },
48
92
  async ({ data }) => {
49
- if (isAuthorized) { return }
50
93
  if (typeof SallaApplePay.detail.shippingContactSelected.onSuccess === 'function') {
51
94
  SallaApplePay.detail.shippingContactSelected.onSuccess(data);
52
95
  }
@@ -57,25 +100,29 @@ export function mutateShipmentAddress(SallaApplePay, shippingContact, isAuthoriz
57
100
  if (!SallaApplePay.shipping_methods || (SallaApplePay.shipping_methods && !SallaApplePay.shipping_methods.length)) {
58
101
  salla.logger.warn('🍏 Pay: We dont found any supported methods', data);
59
102
 
60
- return SallaApplePay.session.completeShippingContactSelection({
103
+ SallaApplePay.session.completeShippingContactSelection({
61
104
  status: SallaApplePay.session.STATUS_INVALID_SHIPPING_POSTAL_ADDRESS,
62
105
  errors: [
63
106
  new window.ApplePayError('addressUnserviceable')
64
107
  ]
65
108
  });
109
+
110
+ return
66
111
  }
67
112
 
68
113
  try {
69
- await SallaApplePay.selectApplePayShippingMethod(SallaApplePay.shipping_methods[0]['ship_id'], SallaApplePay.shipping_methods[0]['private_ship_id']);
114
+ await SallaApplePay.selectApplePayShippingMethod(SallaApplePay.shipping_methods[0]);
70
115
  } catch (error) {
71
116
  salla.logger.warn('Failed set the shipping details to api', error);
72
117
 
73
- return SallaApplePay.session.completeShippingContactSelection({
118
+ SallaApplePay.session.completeShippingContactSelection({
74
119
  status: SallaApplePay.session.STATUS_INVALID_SHIPPING_POSTAL_ADDRESS,
75
120
  errors: [
76
121
  new window.ApplePayError('addressUnserviceable')
77
122
  ]
78
123
  });
124
+
125
+ return;
79
126
  }
80
127
 
81
128
  try {
@@ -83,20 +130,25 @@ export function mutateShipmentAddress(SallaApplePay, shippingContact, isAuthoriz
83
130
  } catch (error) {
84
131
  salla.logger.warn('🍏 Pay: Failed recalculate total', error);
85
132
 
86
- return SallaApplePay.session.completeShippingContactSelection({
133
+ SallaApplePay.session.completeShippingContactSelection({
87
134
  status: SallaApplePay.session.STATUS_INVALID_SHIPPING_POSTAL_ADDRESS,
88
135
  errors: [
89
136
  new window.ApplePayError('addressUnserviceable')
90
137
  ]
91
138
  });
139
+
140
+ return;
92
141
  }
93
142
 
94
- SallaApplePay.session.completeShippingContactSelection({
143
+ const updatedShippingContactSelection = {
95
144
  newTotal: SallaApplePay.prepareTotal(),
96
145
  newLineItems: SallaApplePay.prepareLineItems(),
97
146
  newShippingMethods: SallaApplePay.mappingShippingMethods(SallaApplePay.shipping_methods)
98
- });
147
+ };
99
148
 
149
+ salla.logger.log('🍏 Pay: completeShippingContactSelection', updatedShippingContactSelection);
150
+
151
+ SallaApplePay.session.completeShippingContactSelection(updatedShippingContactSelection);
100
152
  },
101
153
  ({ response }) => {
102
154
  salla.logger.warn('🍏 Pay: Failed add address via api', response);
@@ -108,19 +160,11 @@ export function mutateShipmentAddress(SallaApplePay, shippingContact, isAuthoriz
108
160
  // parse 422 errors
109
161
  let fields = response?.data?.error?.fields;
110
162
 
111
- let errors = [];
112
-
113
- if (fields?.country_code) {
114
- errors.push(new window.ApplePayError('shippingContactInvalid', 'countryCode', fields?.country_code[0]))
115
- }
116
-
117
- if (fields?.city) {
118
- errors.push(new window.ApplePayError('shippingContactInvalid', 'locality', fields?.city[0]))
119
- }
120
-
121
- if (fields?.country) {
122
- errors.push(new window.ApplePayError('shippingContactInvalid', 'country', fields?.country[0]))
123
- }
163
+ let errors = getApplePayErrors({
164
+ countryCode: fields?.country_code && fields.country_code.length > 0 ? fields.country_code[0] : null,
165
+ locality: fields?.city && fields.city.length > 0 ? fields.city[0] : null,
166
+ country: fields?.country && fields.country.length > 0 ? fields.country[0] : null,
167
+ });
124
168
 
125
169
  if (errors.length === 0 && response?.data?.error?.message) {
126
170
  errors.push(new window.ApplePayError('shippingContactInvalid', 'locality', response?.data?.error?.message))
@@ -135,3 +179,53 @@ export function mutateShipmentAddress(SallaApplePay, shippingContact, isAuthoriz
135
179
  }
136
180
  );
137
181
  }
182
+
183
+ function isGuestCheckout() {
184
+ return salla.config.isGuest() && salla.config.get('store.features').includes('guest-checkout');
185
+ }
186
+
187
+ /**
188
+ * Update guest contact
189
+ *
190
+ * @param {SallaApplePay} SallaApplePay
191
+ * @param {ApplePayPaymentContact} shippingContact
192
+ *
193
+ */
194
+ async function updateGuestContact(SallaApplePay, shippingContact) {
195
+ salla.logger.log('🍏 Pay: Updating guest contact', shippingContact);
196
+
197
+ return new Promise((resolve, reject) => {
198
+ http.post(
199
+ SallaApplePay.detail.guestContactSelected.url.replace('{id}', SallaApplePay.id),
200
+ {
201
+ 'email': shippingContact.emailAddress || null,
202
+ 'first_name': shippingContact.givenName || null,
203
+ 'last_name': shippingContact.familyName || null,
204
+ 'phone_number': shippingContact.phoneNumber || null,
205
+ 'country_code': shippingContact.countryCode || null,
206
+ },
207
+ async ({ data }) => {
208
+ if (typeof SallaApplePay.detail.guestContactSelected?.onSuccess === 'function') {
209
+ SallaApplePay.detail.guestContactSelected.onSuccess(data);
210
+ }
211
+ resolve(data);
212
+ },
213
+ ({ response }) => {
214
+ salla.logger.warn('🍏 Pay: Failed to update guest contact via api', response);
215
+
216
+ if (typeof SallaApplePay.detail.guestContactSelected?.onFailed === 'function') {
217
+ SallaApplePay.detail.guestContactSelected.onFailed(response);
218
+ }
219
+
220
+ // Reject the promise so it can be caught in onPaymentAuthorized
221
+ reject({ response });
222
+ }
223
+ );
224
+ });
225
+ }
226
+
227
+ function getApplePayErrors(fields) {
228
+ return Object.entries(fields)
229
+ .filter(([field, messages]) => messages && messages.length > 0)
230
+ .map(([field, messages]) => new window.ApplePayError('shippingContactInvalid', field, messages[0]));
231
+ }