@salla.sa/applepay 3.0.0-alpha.0 → 3.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.
- package/deploy.json +8 -0
- package/package.json +3 -3
- package/src/DetectOS.js +77 -68
- package/src/apple-pay-support.d.ts +24 -0
- package/src/apple-pay-support.js +187 -0
- package/src/http.js +8 -0
- package/src/index.js +818 -77
- package/src/utils.js +119 -25
package/deploy.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salla.sa/applepay",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Salla Apple Pay light",
|
|
5
5
|
"main": "dist/app.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"license": "UNLICENSED",
|
|
25
25
|
"homepage": "https://github.com/SallaApp/twilight#readme",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"axios": "^
|
|
27
|
+
"axios": "^1.10.0"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "43f7a57e278a226a3e1f77924f9da8f10c7e49ba"
|
|
30
30
|
}
|
package/src/DetectOS.js
CHANGED
|
@@ -1,75 +1,84 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 {
|
|
76
|
+
return {
|
|
77
|
+
name: data[i].name,
|
|
78
|
+
version: parseFloat(version),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
74
81
|
}
|
|
82
|
+
return { name: "unknown", version: 0 };
|
|
83
|
+
},
|
|
75
84
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const APPLE_PAY_METHOD: string;
|
|
2
|
+
|
|
3
|
+
export declare function applePayMerchantId(): string | null;
|
|
4
|
+
|
|
5
|
+
export declare function buildApplePayMethodData(opts: {
|
|
6
|
+
version: number;
|
|
7
|
+
merchantIdentifier: string;
|
|
8
|
+
supportedNetworks: string[];
|
|
9
|
+
countryCode: string;
|
|
10
|
+
}): PaymentMethodData[];
|
|
11
|
+
|
|
12
|
+
export declare function isApplePayHostedRedirect(): boolean;
|
|
13
|
+
|
|
14
|
+
export declare function ensureApplePaySdk(): Promise<void>;
|
|
15
|
+
|
|
16
|
+
export declare function normalizeApplePayNetworks(raw: string[] | Record<string, string> | null | undefined): string[];
|
|
17
|
+
|
|
18
|
+
export declare function applePayNetworks(): string[];
|
|
19
|
+
|
|
20
|
+
export declare function resolveApplePayNetworks(
|
|
21
|
+
networks: string[] | Record<string, string> | null | undefined,
|
|
22
|
+
): string[];
|
|
23
|
+
|
|
24
|
+
export declare function canCompleteApplePay(currency?: string, networks?: string[] | null): Promise<boolean>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Apple Pay support helpers: SDK loading, capability detection, merchant
|
|
3
|
+
* identifier resolution, and Payment Request method-data construction.
|
|
4
|
+
*
|
|
5
|
+
* Lives in @salla.sa/applepay (not a theme) because it is Apple Pay domain
|
|
6
|
+
* logic and is consumed by every Apple Pay entry point — the storefront
|
|
7
|
+
* components (salla-quick-buy, salla-add-product-button) and the shared
|
|
8
|
+
* SallaApplePay session in ./index.js.
|
|
9
|
+
*
|
|
10
|
+
* Apple Pay runs through two flows:
|
|
11
|
+
* - Native (Safari / Apple devices) → window.ApplePaySession
|
|
12
|
+
* - Payment Request API (other browsers) → Apple's "Scan Code with iPhone"
|
|
13
|
+
* QR sheet, which requires Apple's JS SDK to be *loaded and registered*
|
|
14
|
+
* before PaymentRequest.canMakePayment() reports the method as available.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const APPLE_PAY_METHOD = 'https://apple.com/apple-pay';
|
|
18
|
+
const APPLE_PAY_SDK_ID = 'apple-pay-sdk';
|
|
19
|
+
const APPLE_PAY_SDK_SRC = 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js';
|
|
20
|
+
|
|
21
|
+
let sdkPromise = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Salla-native Apple merchant identifier, sourced from store settings. Returns
|
|
25
|
+
* null when the store has no Apple Pay merchant configured — callers treat that
|
|
26
|
+
* absence as the guard and skip Apple Pay entirely (no hardcoded fallback).
|
|
27
|
+
* @returns {string|null}
|
|
28
|
+
*/
|
|
29
|
+
export function applePayMerchantId() {
|
|
30
|
+
return salla?.config?.get?.('store.settings.buy_now.merchant_identifier') || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the store's Apple Pay is a hosted redirect rather than a native sheet.
|
|
35
|
+
*
|
|
36
|
+
* MyFatoorah Apple Pay without S2S has no native flow: the checkout page hands off as a
|
|
37
|
+
* credit_card redirect to MyFatoorah's hosted page. The buy-now surfaces (PDP, sticky bar)
|
|
38
|
+
* have no redirect path, so they must not offer an Apple Pay button at all.
|
|
39
|
+
*
|
|
40
|
+
* The backend publishes this as a single boolean (`ApplePayFeature::isHostedRedirect`).
|
|
41
|
+
* Compared with `=== true` on purpose: against a backend that predates the field the key is
|
|
42
|
+
* absent, so this returns false and every surface keeps its current behaviour. That is what
|
|
43
|
+
* makes the storefront half safe to ship independently of the backend half.
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export function isApplePayHostedRedirect() {
|
|
47
|
+
return salla?.config?.get?.('store.settings.buy_now.hostedRedirect') === true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the W3C Payment Request `methodData` for the Apple Pay method. Callers
|
|
52
|
+
* supply the values (the probe reads them from config; the live session reads
|
|
53
|
+
* them from the transaction detail), this owns the constant scaffold so the two
|
|
54
|
+
* can't drift apart.
|
|
55
|
+
* @param {{ version: number, merchantIdentifier: string, supportedNetworks: string[], countryCode: string }} opts
|
|
56
|
+
* @returns {PaymentMethodData[]}
|
|
57
|
+
*/
|
|
58
|
+
export function buildApplePayMethodData({ version, merchantIdentifier, supportedNetworks, countryCode }) {
|
|
59
|
+
return [{
|
|
60
|
+
supportedMethods: APPLE_PAY_METHOD,
|
|
61
|
+
data: {
|
|
62
|
+
version,
|
|
63
|
+
merchantIdentifier,
|
|
64
|
+
merchantCapabilities: ['supports3DS'],
|
|
65
|
+
supportedNetworks,
|
|
66
|
+
countryCode,
|
|
67
|
+
},
|
|
68
|
+
}];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Inject Apple's Pay JS SDK (`1.latest` ships the cross-browser / QR support)
|
|
73
|
+
* and resolve once it has loaded. Idempotent: a second call reuses the same
|
|
74
|
+
* in-flight promise and never appends a second <script>. Resolves (rather than
|
|
75
|
+
* rejects) on load error so a failed CDN fetch simply falls back to "no QR".
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
export function ensureApplePaySdk() {
|
|
79
|
+
if (typeof document === 'undefined') return Promise.resolve();
|
|
80
|
+
|
|
81
|
+
if (sdkPromise) return sdkPromise;
|
|
82
|
+
|
|
83
|
+
const existing = document.getElementById(APPLE_PAY_SDK_ID);
|
|
84
|
+
if (existing) {
|
|
85
|
+
// Script already in the DOM (e.g. added before this refactor). We can't know
|
|
86
|
+
// if it finished loading, so resolve immediately — the native/PaymentRequest
|
|
87
|
+
// probe will just report unavailable until the browser registers the method.
|
|
88
|
+
sdkPromise = Promise.resolve();
|
|
89
|
+
return sdkPromise;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
sdkPromise = new Promise((resolve) => {
|
|
93
|
+
const script = document.createElement('script');
|
|
94
|
+
script.src = APPLE_PAY_SDK_SRC;
|
|
95
|
+
script.setAttribute('id', APPLE_PAY_SDK_ID);
|
|
96
|
+
script.async = true;
|
|
97
|
+
script.onload = () => resolve();
|
|
98
|
+
script.onerror = () => resolve();
|
|
99
|
+
document.body.appendChild(script);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return sdkPromise;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Coerce a networks payload into an array. PHP serializes the list as an object when the
|
|
107
|
+
* array keys are non-sequential, so both shapes arrive.
|
|
108
|
+
*
|
|
109
|
+
* @param {string[]|Object|null|undefined} raw
|
|
110
|
+
* @returns {string[]}
|
|
111
|
+
*/
|
|
112
|
+
export function normalizeApplePayNetworks(raw) {
|
|
113
|
+
if (Array.isArray(raw)) return [...raw];
|
|
114
|
+
if (raw && typeof raw === 'object') return Object.values(raw);
|
|
115
|
+
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The store's buy-now networks from config.
|
|
121
|
+
*
|
|
122
|
+
* @returns {string[]}
|
|
123
|
+
*/
|
|
124
|
+
export function applePayNetworks() {
|
|
125
|
+
return normalizeApplePayNetworks(
|
|
126
|
+
salla?.config?.get?.('store.settings.buy_now.supportedNetworks')
|
|
127
|
+
|| salla?.config?.get?.('store.settings.buy_now.networks'),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The networks for this attempt. `null`/`undefined` means "not supplied" and defers to config;
|
|
133
|
+
* an array — empty included — is the backend's answer and is used as-is.
|
|
134
|
+
*
|
|
135
|
+
* @param {string[]|Object|null|undefined} networks
|
|
136
|
+
* @returns {string[]}
|
|
137
|
+
*/
|
|
138
|
+
export function resolveApplePayNetworks(networks) {
|
|
139
|
+
return networks == null ? applePayNetworks() : normalizeApplePayNetworks(networks);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether Apple Pay can be completed in the current browser: natively
|
|
144
|
+
* (Safari / Apple devices) or via the Payment Request API (non-Safari browsers
|
|
145
|
+
* / non-Apple devices → QR-code fallback).
|
|
146
|
+
*
|
|
147
|
+
* For the QR path this awaits the SDK before probing, so callers get a stable
|
|
148
|
+
* answer on first load instead of racing the async script fetch.
|
|
149
|
+
* @param {string} [currency]
|
|
150
|
+
* @param {string[]} [networks] The backend's list; null/undefined defers to the buy-now config.
|
|
151
|
+
* @returns {Promise<boolean>}
|
|
152
|
+
*/
|
|
153
|
+
export async function canCompleteApplePay(currency = 'SAR', networks = null) {
|
|
154
|
+
const supportedNetworks = resolveApplePayNetworks(networks);
|
|
155
|
+
|
|
156
|
+
// Nothing to charge with — checked before the native probe, which would report "available"
|
|
157
|
+
// and only fail after the shopper taps pay.
|
|
158
|
+
if (!supportedNetworks.length) return false;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
if (window.ApplePaySession?.canMakePayments?.()) return true;
|
|
162
|
+
} catch (e) { /* not supported natively */ }
|
|
163
|
+
|
|
164
|
+
if (typeof window === 'undefined' || !window.PaymentRequest) return false;
|
|
165
|
+
|
|
166
|
+
// QR path only: it can't validate the merchant without a merchant identifier,
|
|
167
|
+
// so treat a missing config key as the guard. Native (above) is unaffected —
|
|
168
|
+
// it never uses the client-side merchant id.
|
|
169
|
+
if (!applePayMerchantId()) return false;
|
|
170
|
+
|
|
171
|
+
await ensureApplePaySdk();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const request = new PaymentRequest(
|
|
175
|
+
buildApplePayMethodData({
|
|
176
|
+
version: 3,
|
|
177
|
+
merchantIdentifier: applePayMerchantId(),
|
|
178
|
+
supportedNetworks,
|
|
179
|
+
countryCode: salla?.config?.get?.('store.store_country') || 'SA',
|
|
180
|
+
}),
|
|
181
|
+
{ total: { label: 'Salla', amount: { currency: currency || 'SAR', value: '0.00' } } },
|
|
182
|
+
);
|
|
183
|
+
return await request.canMakePayment();
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/http.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
2
|
|
|
3
|
+
function setBaseCurrencyHeader() {
|
|
4
|
+
axios.defaults.headers.common.currency = salla?.config?.get?.('user.currency_code');
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export default {
|
|
4
8
|
request(method, url, data, successCb = null, errorCb = null) {
|
|
9
|
+
setBaseCurrencyHeader();
|
|
5
10
|
return axios
|
|
6
11
|
.request({url, data, method: method.toLowerCase(), responseType: 'json'})
|
|
7
12
|
.then(successCb)
|
|
@@ -10,6 +15,7 @@ export default {
|
|
|
10
15
|
|
|
11
16
|
get(url, successCb = null, errorCb = null, data) {
|
|
12
17
|
// return this.request('get', url, data, successCb, errorCb);
|
|
18
|
+
setBaseCurrencyHeader();
|
|
13
19
|
return axios
|
|
14
20
|
.get(url, {params: data})
|
|
15
21
|
.then(successCb)
|
|
@@ -41,10 +47,12 @@ export default {
|
|
|
41
47
|
/**
|
|
42
48
|
* @deprecated to support legacy themes
|
|
43
49
|
*/
|
|
50
|
+
setBaseCurrencyHeader();
|
|
44
51
|
$.ajax({
|
|
45
52
|
url: url,
|
|
46
53
|
method: method.toUpperCase(),
|
|
47
54
|
data: payload,
|
|
55
|
+
headers: { currency: axios.defaults.headers.common.currency },
|
|
48
56
|
async: false,
|
|
49
57
|
success: function ({data}) {
|
|
50
58
|
return resolve(data);
|