@savers_app/react-native-sandbox-sdk 1.2.7 → 1.2.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.
package/README.md CHANGED
@@ -88,7 +88,8 @@ SaversAppSDK.initialized({
88
88
  apiKey: 'YOUR_API_KEY',
89
89
  encryptionKey: 'BASE64_256_BIT_ENCRYPTION_KEY',
90
90
  pRefCode: 'PROGRAM_REF_CODE',
91
- authMode: 'CUSTOMER_SIGN_IN_UP_MODE' // 'EMAIL' | 'PHONE'
91
+ authMode: 'CUSTOMER_SIGN_IN_UP_MODE', // 'EMAIL' | 'PHONE'
92
+ environment: 'PROD', // optional: 'SANDBOX' (https://testm.saversapp.com) | 'PROD' (https://m.saversapp.com) for generateUrl
92
93
  });
93
94
  ```
94
95
 
@@ -142,7 +143,7 @@ const url = await generateUrl({
142
143
  // sessionId is optional; when omitted, the SDK uses its stored session id
143
144
  // deviceInfo is optional; the SDK builds it internally using device id (and optional coordinates)
144
145
  });
145
- // returns https://m.saversapp.com/?pRefCode=...&qP=...
146
+ // returns https://m.saversapp.com/... or https://testm.saversapp.com/... depending on `environment` in init (default follows build-time config)
146
147
  ```
147
148
 
148
149
  Profile requirements:
@@ -187,6 +188,7 @@ export function App() {
187
188
  pRefCode: 'PROGRAM_REF_CODE',
188
189
  authMode: 'EMAIL', // or 'PHONE'
189
190
  navigationRef, // pass the ref to the SDK
191
+ environment: 'PROD', // optional: 'SANDBOX' | 'PROD' for generateUrl hosted origin
190
192
  });
191
193
 
192
194
  return <NavigationContainer ref={navigationRef}>{/* your navigators */}</NavigationContainer>;
@@ -9,9 +9,14 @@ import { setApiKey, setEncryptionKey, setPRefCode, setAuthMode } from "../data/s
9
9
  import { getDeviceId } from "../data/storage/deviceIdManager.js";
10
10
  import { setReactRef } from "../data/storage/reactRefManager.js";
11
11
 
12
+ // Hosted app base URL (test vs production merchant web)
13
+ import { setEnvironment } from "../utils/config.js";
14
+
12
15
  // Services
13
16
  // import { getDeviceLocation } from '../services/device/location';
14
17
 
18
+ /** Controls which hosted merchant web origin `generateUrl` uses. */
19
+
15
20
  export function initializeSDK(providedKeys) {
16
21
  const keys = providedKeys;
17
22
  const missing = [];
@@ -25,6 +30,15 @@ export function initializeSDK(providedKeys) {
25
30
  logger.warn(msg);
26
31
  throw new Error(msg);
27
32
  }
33
+ if (keys.environment !== undefined) {
34
+ if (keys.environment !== 'SANDBOX' && keys.environment !== 'PROD') {
35
+ throw new Error(`[SDK] Invalid environment: ${String(keys.environment)}. Use 'SANDBOX' or 'PROD'.`);
36
+ }
37
+ setEnvironment(keys.environment === 'SANDBOX' ? 'sandbox' : 'production');
38
+ logger.info('[SDK] Hosted app environment', {
39
+ environment: keys.environment
40
+ });
41
+ }
28
42
  setApiKey(keys.apiKey);
29
43
  setEncryptionKey(keys.encryptionKey);
30
44
  setPRefCode(keys.pRefCode);
@@ -1 +1 @@
1
- {"version":3,"names":["logger","SDKRequirements","setApiKey","setEncryptionKey","setPRefCode","setAuthMode","getDeviceId","setReactRef","initializeSDK","providedKeys","keys","missing","apiKey","push","encryptionKey","pRefCode","authMode","navigationRef","length","msg","join","warn","Error","then","id","info","deviceId","Boolean","catch","asyncStorage","geolocation","deviceInfo","SaversAppSDK","initialized"],"sourceRoot":"../../../src","sources":["core/runtime.ts"],"mappings":";;AAAA;AACA,SAASA,MAAM,QAAQ,oBAAiB;AACxC,SAASC,eAAe,QAAQ,+BAA4B;;AAE5D;AACA,SACEC,SAAS,EACTC,gBAAgB,EAChBC,WAAW,EACXC,WAAW,QACN,gCAA6B;AACpC,SAASC,WAAW,QAAQ,oCAAiC;AAC7D,SAASC,WAAW,QAAQ,oCAAiC;;AAE7D;AACA;;AAEA,OAAO,SAASC,aAAaA,CAACC,YAM7B,EAAE;EACD,MAAMC,IAAI,GAAGD,YAAY;EACzB,MAAME,OAAiB,GAAG,EAAE;EAC5B,IAAI,CAACD,IAAI,CAACE,MAAM,EAAED,OAAO,CAACE,IAAI,CAAC,QAAQ,CAAC;EACxC,IAAI,CAACH,IAAI,CAACI,aAAa,EAAEH,OAAO,CAACE,IAAI,CAAC,eAAe,CAAC;EACtD,IAAI,CAACH,IAAI,CAACK,QAAQ,EAAEJ,OAAO,CAACE,IAAI,CAAC,UAAU,CAAC;EAC5C,IAAI,CAACH,IAAI,CAACM,QAAQ,EAAEL,OAAO,CAACE,IAAI,CAAC,UAAU,CAAC;EAC5C,IAAI,CAACH,IAAI,CAACO,aAAa,EAAEN,OAAO,CAACE,IAAI,CAAC,eAAe,CAAC;EAEtD,IAAIF,OAAO,CAACO,MAAM,EAAE;IAClB,MAAMC,GAAG,GAAG,gCAAgCR,OAAO,CAACS,IAAI,CAAC,IAAI,CAAC,EAAE;IAChEpB,MAAM,CAACqB,IAAI,CAACF,GAAG,CAAC;IAChB,MAAM,IAAIG,KAAK,CAACH,GAAG,CAAC;EACtB;EACAjB,SAAS,CAACQ,IAAI,CAACE,MAAM,CAAC;EACtBT,gBAAgB,CAACO,IAAI,CAACI,aAAa,CAAC;EACpCV,WAAW,CAACM,IAAI,CAACK,QAAQ,CAAC;EAC1BV,WAAW,CAACK,IAAI,CAACM,QAAQ,CAAC;EAC1BT,WAAW,CAACG,IAAI,CAACO,aAAa,CAAC;EAE/BX,WAAW,CAAC,CAAC,CACViB,IAAI,CAAEC,EAAE,IAAK;IACZxB,MAAM,CAACyB,IAAI,CAAC,8BAA8B,EAAE;MAAEC,QAAQ,EAAEC,OAAO,CAACH,EAAE;IAAE,CAAC,CAAC;EACxE,CAAC,CAAC,CACDI,KAAK,CAAC,MAAM;IACX5B,MAAM,CAACqB,IAAI,CAAC,6BAA6B,CAAC;EAC5C,CAAC,CAAC;;EAEJ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA,MAAM;IAAEQ,YAAY;IAAEC,WAAW;IAAEC;EAAW,CAAC,GAAG9B,eAAe;EACjE,IAAI4B,YAAY,IAAIC,WAAW,IAAIC,UAAU,EAAE;EAC/C,IAAI,CAACF,YAAY,EAAE;IACjB7B,MAAM,CAACqB,IAAI,CACT,4GACF,CAAC;EACH;EACA,IAAI,CAACS,WAAW,EAAE;IAChB9B,MAAM,CAACqB,IAAI,CACT,+FACF,CAAC;EACH;EAEA,IAAI,CAACU,UAAU,EAAE;IACf/B,MAAM,CAACqB,IAAI,CACT,uFACF,CAAC;EACH;AACF;AAEA,OAAO,MAAMW,YAAY,GAAG;EAC1BC,WAAW,EAAEzB;AACf,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["logger","SDKRequirements","setApiKey","setEncryptionKey","setPRefCode","setAuthMode","getDeviceId","setReactRef","setEnvironment","initializeSDK","providedKeys","keys","missing","apiKey","push","encryptionKey","pRefCode","authMode","navigationRef","length","msg","join","warn","Error","environment","undefined","String","info","then","id","deviceId","Boolean","catch","asyncStorage","geolocation","deviceInfo","SaversAppSDK","initialized"],"sourceRoot":"../../../src","sources":["core/runtime.ts"],"mappings":"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","ignoreList":[]}
@@ -8,6 +8,8 @@ export * from "./utils/errors.js";
8
8
  export * from "./utils/logger.js";
9
9
  export * from "./utils/validator.js";
10
10
  export * from "./services/webview/messageHandler.js";
11
+ export * from "./services/webview/DualWebViewBridgeController.js";
12
+ export * from "./services/webview/dualWebViewBridge.types.js";
11
13
  export * from "./data/storage/deviceIdManager.js";
12
14
  export * from "./services/navigation/dialPad.js";
13
15
  export * from "./services/navigation/openBrowser.js";
@@ -1 +1 @@
1
- {"version":3,"names":[],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,cAAc,kCAA+B;AAC7C,cAAc,2CAAwC;AACtD,cAAc,6CAA0C;AACxD,cAAc,4BAAyB;AACvC,cAAc,mBAAgB;AAC9B,cAAc,mBAAgB;AAC9B,cAAc,sBAAmB;AACjC,cAAc,sCAAmC;AACjD,cAAc,mCAAgC;AAC9C,cAAc,kCAA+B;AAC7C,cAAc,sCAAmC;AACjD,cAAc,kCAA+B;AAC7C,cAAc,gCAA6B;AAC3C,cAAc,8BAA2B;AACzC,cAAc,mBAAgB;AAC9B,cAAc,+BAA4B;AAC1C,cAAc,mCAAgC","ignoreList":[]}
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,cAAc,kCAA+B;AAC7C,cAAc,2CAAwC;AACtD,cAAc,6CAA0C;AACxD,cAAc,4BAAyB;AACvC,cAAc,mBAAgB;AAC9B,cAAc,mBAAgB;AAC9B,cAAc,sBAAmB;AACjC,cAAc,sCAAmC;AACjD,cAAc,mDAAgD;AAC9D,cAAc,+CAA4C;AAC1D,cAAc,mCAAgC;AAC9C,cAAc,kCAA+B;AAC7C,cAAc,sCAAmC;AACjD,cAAc,kCAA+B;AAC7C,cAAc,gCAA6B;AAC3C,cAAc,8BAA2B;AACzC,cAAc,mBAAgB;AAC9B,cAAc,+BAA4B;AAC1C,cAAc,mCAAgC","ignoreList":[]}
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
4
+ import { Image, StyleSheet, TouchableOpacity, View } from 'react-native';
5
+ import WebView from 'react-native-webview';
6
+ import { parseDualWebViewEnvelope } from "./dualWebViewBridge.types.js";
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ const DEFAULT_TRAVEL_PORTAL_URL = 'https://sandbox.travelercashback.com';
9
+
10
+ /** Must match `applicationNameForUserAgent` on clo-app WebViews (DualWebViewBridgeController). */
11
+ const WEBVIEW_UA_MARKER = 'CloAppWebView';
12
+
13
+ /** Legacy / Hub: open travel from first WebView without JSON envelope. */
14
+ const TRAVEL_DEEP_LINK = 'saversapp://travel';
15
+ const LEGACY_OPEN_TRAVEL_ACTIONS = ['OPEN_TRAVEL_PORTAL', 'OPEN_TRAVEL'];
16
+ function buildInjectNativeBridgeScript(detail) {
17
+ return `
18
+ (function(){
19
+ var detail = ${JSON.stringify(detail)};
20
+ window.dispatchEvent(new CustomEvent('nativeBridge', { detail: detail }));
21
+ true;
22
+ })();
23
+ `;
24
+ }
25
+ export const DualWebViewBridgeController = ({
26
+ saversAppUrl,
27
+ travelPortalUrl = DEFAULT_TRAVEL_PORTAL_URL,
28
+ partnerLogo,
29
+ renderTravelBackIcon,
30
+ initialSurface = 'savers',
31
+ onSaversSdkMessage,
32
+ onLoadEndSavers,
33
+ onLoadEndTravel
34
+ }) => {
35
+ const saversRef = useRef(null);
36
+ const travelRef = useRef(null);
37
+ const [surface, setSurface] = useState(initialSurface);
38
+ const [saversKey, setSaversKey] = useState(0);
39
+ const [travelKey, setTravelKey] = useState(0);
40
+ const openTravel = useCallback(() => {
41
+ setSurface('travel');
42
+ setTravelKey(k => k + 1);
43
+ }, []);
44
+ const backToSavers = useCallback(() => {
45
+ setSurface('savers');
46
+ setSaversKey(k => k + 1);
47
+ }, []);
48
+ const relayTo = useCallback((target, payload, from) => {
49
+ const detail = {
50
+ bridge: 'dual-webview',
51
+ from,
52
+ action: 'relay',
53
+ payload
54
+ };
55
+ const ref = target === 'savers' ? saversRef : travelRef;
56
+ ref.current?.injectJavaScript(buildInjectNativeBridgeScript(detail));
57
+ }, []);
58
+ const handleBridgeEnvelope = useCallback(env => {
59
+ switch (env.action) {
60
+ case 'open_travel':
61
+ openTravel();
62
+ return true;
63
+ case 'close_travel':
64
+ backToSavers();
65
+ return true;
66
+ case 'relay':
67
+ if (env.to === 'savers') {
68
+ relayTo('savers', env.payload, env.from);
69
+ return true;
70
+ }
71
+ if (env.to === 'travel') {
72
+ relayTo('travel', env.payload, env.from);
73
+ return true;
74
+ }
75
+ return false;
76
+ default:
77
+ return false;
78
+ }
79
+ }, [backToSavers, openTravel, relayTo]);
80
+ const tryLegacyOpenTravel = useCallback(raw => {
81
+ if (!raw || typeof raw !== 'string') {
82
+ return false;
83
+ }
84
+ try {
85
+ const data = JSON.parse(raw);
86
+ if (data?.action === 'OPEN_TRAVEL_PORTAL' || data?.type === 'OPEN_TRAVEL' || data?.openTravel === true) {
87
+ openTravel();
88
+ return true;
89
+ }
90
+ } catch {
91
+ /* ignore */
92
+ }
93
+ if (LEGACY_OPEN_TRAVEL_ACTIONS.some(s => raw.includes(s))) {
94
+ openTravel();
95
+ return true;
96
+ }
97
+ return false;
98
+ }, [openTravel]);
99
+ const onMessageSavers = useCallback(e => {
100
+ const raw = e?.nativeEvent?.data ?? '';
101
+ const env = parseDualWebViewEnvelope(raw);
102
+ if (env && env.from === 'savers' && handleBridgeEnvelope(env)) {
103
+ return;
104
+ }
105
+ if (tryLegacyOpenTravel(raw)) {
106
+ return;
107
+ }
108
+ const postBack = data => {
109
+ const detail = {
110
+ bridge: 'dual-webview',
111
+ from: 'native',
112
+ action: 'relay',
113
+ payload: data
114
+ };
115
+ saversRef.current?.injectJavaScript(buildInjectNativeBridgeScript(detail));
116
+ };
117
+ onSaversSdkMessage?.(raw, postBack);
118
+ }, [handleBridgeEnvelope, onSaversSdkMessage, tryLegacyOpenTravel]);
119
+ const onMessageTravel = useCallback(e => {
120
+ const raw = e?.nativeEvent?.data ?? '';
121
+ const env = parseDualWebViewEnvelope(raw);
122
+ if (env && env.from === 'travel' && handleBridgeEnvelope(env)) {
123
+ return;
124
+ }
125
+ if (tryLegacyOpenTravel(raw)) {
126
+ return;
127
+ }
128
+ }, [handleBridgeEnvelope, tryLegacyOpenTravel]);
129
+ const onShouldStartLoadSavers = useCallback(request => {
130
+ const {
131
+ url
132
+ } = request;
133
+ if (url.startsWith(TRAVEL_DEEP_LINK) || url === 'saversapp://travel/') {
134
+ openTravel();
135
+ return false;
136
+ }
137
+ return true;
138
+ }, [openTravel]);
139
+ const travelSource = useMemo(() => ({
140
+ uri: travelPortalUrl
141
+ }), [travelPortalUrl]);
142
+ const saversSource = useMemo(() => ({
143
+ uri: saversAppUrl
144
+ }), [saversAppUrl]);
145
+ return /*#__PURE__*/_jsxs(View, {
146
+ style: styles.root,
147
+ children: [surface === 'savers' ? /*#__PURE__*/_jsx(WebView, {
148
+ ref: saversRef,
149
+ source: saversSource,
150
+ originWhitelist: ['*'],
151
+ applicationNameForUserAgent: WEBVIEW_UA_MARKER,
152
+ onMessage: onMessageSavers,
153
+ onShouldStartLoadWithRequest: onShouldStartLoadSavers,
154
+ onLoadEnd: onLoadEndSavers,
155
+ style: styles.webview,
156
+ sharedCookiesEnabled: true,
157
+ incognito: false,
158
+ webviewDebuggingEnabled: __DEV__
159
+ }, saversKey) : null, surface === 'travel' ? /*#__PURE__*/_jsxs(View, {
160
+ style: styles.travelColumn,
161
+ children: [/*#__PURE__*/_jsxs(View, {
162
+ style: styles.header,
163
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
164
+ onPress: backToSavers,
165
+ hitSlop: 16,
166
+ accessibilityRole: "button",
167
+ accessibilityLabel: "Back",
168
+ style: styles.backButton,
169
+ children: renderTravelBackIcon ? renderTravelBackIcon() : /*#__PURE__*/_jsxs(View, {
170
+ style: styles.backIconWrapper,
171
+ children: [/*#__PURE__*/_jsx(View, {
172
+ style: styles.backIconShaft
173
+ }), /*#__PURE__*/_jsx(View, {
174
+ style: styles.backIconHead
175
+ })]
176
+ })
177
+ }), /*#__PURE__*/_jsx(View, {
178
+ style: styles.headerSpacer
179
+ }), partnerLogo ? /*#__PURE__*/_jsx(Image, {
180
+ source: {
181
+ uri: partnerLogo
182
+ },
183
+ resizeMode: "contain",
184
+ style: styles.logo
185
+ }) : /*#__PURE__*/_jsx(View, {
186
+ style: styles.logoPlaceholder
187
+ })]
188
+ }), /*#__PURE__*/_jsx(WebView, {
189
+ ref: travelRef,
190
+ source: travelSource,
191
+ originWhitelist: ['*'],
192
+ applicationNameForUserAgent: WEBVIEW_UA_MARKER,
193
+ onMessage: onMessageTravel,
194
+ onLoadEnd: onLoadEndTravel,
195
+ style: styles.webview,
196
+ sharedCookiesEnabled: true,
197
+ incognito: false,
198
+ webviewDebuggingEnabled: __DEV__
199
+ }, travelKey)]
200
+ }) : null]
201
+ });
202
+ };
203
+ const styles = StyleSheet.create({
204
+ root: {
205
+ flex: 1
206
+ },
207
+ travelColumn: {
208
+ flex: 1
209
+ },
210
+ webview: {
211
+ flex: 1
212
+ },
213
+ header: {
214
+ flexDirection: 'row',
215
+ alignItems: 'center',
216
+ backgroundColor: '#fff',
217
+ paddingHorizontal: 16,
218
+ paddingTop: 12,
219
+ paddingBottom: 12,
220
+ borderBottomWidth: StyleSheet.hairlineWidth,
221
+ borderBottomColor: '#E5E5E5'
222
+ },
223
+ backButton: {
224
+ width: 32,
225
+ height: 32,
226
+ justifyContent: 'center',
227
+ alignItems: 'center'
228
+ },
229
+ backIconWrapper: {
230
+ width: 20,
231
+ height: 16,
232
+ justifyContent: 'center',
233
+ position: 'relative'
234
+ },
235
+ backIconShaft: {
236
+ position: 'absolute',
237
+ left: 6,
238
+ right: 0,
239
+ height: 2,
240
+ backgroundColor: '#111827',
241
+ borderRadius: 1
242
+ },
243
+ backIconHead: {
244
+ width: 10,
245
+ height: 10,
246
+ borderLeftWidth: 2,
247
+ borderBottomWidth: 2,
248
+ borderColor: '#111827',
249
+ transform: [{
250
+ rotate: '45deg'
251
+ }],
252
+ marginLeft: 1
253
+ },
254
+ headerSpacer: {
255
+ flex: 1
256
+ },
257
+ logo: {
258
+ width: 120,
259
+ height: 32
260
+ },
261
+ logoPlaceholder: {
262
+ width: 120,
263
+ height: 32
264
+ }
265
+ });
266
+ export default DualWebViewBridgeController;
267
+ //# sourceMappingURL=DualWebViewBridgeController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","useCallback","useMemo","useRef","useState","Image","StyleSheet","TouchableOpacity","View","WebView","parseDualWebViewEnvelope","jsx","_jsx","jsxs","_jsxs","DEFAULT_TRAVEL_PORTAL_URL","WEBVIEW_UA_MARKER","TRAVEL_DEEP_LINK","LEGACY_OPEN_TRAVEL_ACTIONS","buildInjectNativeBridgeScript","detail","JSON","stringify","DualWebViewBridgeController","saversAppUrl","travelPortalUrl","partnerLogo","renderTravelBackIcon","initialSurface","onSaversSdkMessage","onLoadEndSavers","onLoadEndTravel","saversRef","travelRef","surface","setSurface","saversKey","setSaversKey","travelKey","setTravelKey","openTravel","k","backToSavers","relayTo","target","payload","from","bridge","action","ref","current","injectJavaScript","handleBridgeEnvelope","env","to","tryLegacyOpenTravel","raw","data","parse","type","some","s","includes","onMessageSavers","e","nativeEvent","postBack","onMessageTravel","onShouldStartLoadSavers","request","url","startsWith","travelSource","uri","saversSource","style","styles","root","children","source","originWhitelist","applicationNameForUserAgent","onMessage","onShouldStartLoadWithRequest","onLoadEnd","webview","sharedCookiesEnabled","incognito","webviewDebuggingEnabled","__DEV__","travelColumn","header","onPress","hitSlop","accessibilityRole","accessibilityLabel","backButton","backIconWrapper","backIconShaft","backIconHead","headerSpacer","resizeMode","logo","logoPlaceholder","create","flex","flexDirection","alignItems","backgroundColor","paddingHorizontal","paddingTop","paddingBottom","borderBottomWidth","hairlineWidth","borderBottomColor","width","height","justifyContent","position","left","right","borderRadius","borderLeftWidth","borderColor","transform","rotate","marginLeft"],"sourceRoot":"../../../../src","sources":["services/webview/DualWebViewBridgeController.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,WAAW,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AACrE,SAASC,KAAK,EAAEC,UAAU,EAAEC,gBAAgB,EAAEC,IAAI,QAAQ,cAAc;AACxE,OAAOC,OAAO,MAAM,sBAAsB;AAC1C,SAEEC,wBAAwB,QACnB,8BAA2B;AAAC,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAEnC,MAAMC,yBAAyB,GAAG,sCAAsC;;AAExE;AACA,MAAMC,iBAAiB,GAAG,eAAe;;AAEzC;AACA,MAAMC,gBAAgB,GAAG,oBAAoB;AAC7C,MAAMC,0BAA0B,GAAG,CAAC,oBAAoB,EAAE,aAAa,CAAC;AAexE,SAASC,6BAA6BA,CACpCC,MAA+B,EACvB;EACR,OAAO;AACT;AACA,qBAAqBC,IAAI,CAACC,SAAS,CAACF,MAAM,CAAC;AAC3C;AACA;AACA;AACA,GAAG;AACH;AAEA,OAAO,MAAMG,2BAEZ,GAAGA,CAAC;EACHC,YAAY;EACZC,eAAe,GAAGV,yBAAyB;EAC3CW,WAAW;EACXC,oBAAoB;EACpBC,cAAc,GAAG,QAAQ;EACzBC,kBAAkB;EAClBC,eAAe;EACfC;AACF,CAAC,KAAK;EACJ,MAAMC,SAAS,GAAG7B,MAAM,CAAU,IAAI,CAAC;EACvC,MAAM8B,SAAS,GAAG9B,MAAM,CAAU,IAAI,CAAC;EAEvC,MAAM,CAAC+B,OAAO,EAAEC,UAAU,CAAC,GAAG/B,QAAQ,CAAUwB,cAAc,CAAC;EAC/D,MAAM,CAACQ,SAAS,EAAEC,YAAY,CAAC,GAAGjC,QAAQ,CAAC,CAAC,CAAC;EAC7C,MAAM,CAACkC,SAAS,EAAEC,YAAY,CAAC,GAAGnC,QAAQ,CAAC,CAAC,CAAC;EAE7C,MAAMoC,UAAU,GAAGvC,WAAW,CAAC,MAAM;IACnCkC,UAAU,CAAC,QAAQ,CAAC;IACpBI,YAAY,CAAEE,CAAC,IAAKA,CAAC,GAAG,CAAC,CAAC;EAC5B,CAAC,EAAE,EAAE,CAAC;EAEN,MAAMC,YAAY,GAAGzC,WAAW,CAAC,MAAM;IACrCkC,UAAU,CAAC,QAAQ,CAAC;IACpBE,YAAY,CAAEI,CAAC,IAAKA,CAAC,GAAG,CAAC,CAAC;EAC5B,CAAC,EAAE,EAAE,CAAC;EAEN,MAAME,OAAO,GAAG1C,WAAW,CACzB,CACE2C,MAA2B,EAC3BC,OAAgB,EAChBC,IAAyB,KACtB;IACH,MAAM1B,MAAM,GAAG;MACb2B,MAAM,EAAE,cAAc;MACtBD,IAAI;MACJE,MAAM,EAAE,OAAgB;MACxBH;IACF,CAAC;IACD,MAAMI,GAAG,GAAGL,MAAM,KAAK,QAAQ,GAAGZ,SAAS,GAAGC,SAAS;IACvDgB,GAAG,CAACC,OAAO,EAAEC,gBAAgB,CAAChC,6BAA6B,CAACC,MAAM,CAAC,CAAC;EACtE,CAAC,EACD,EACF,CAAC;EAED,MAAMgC,oBAAoB,GAAGnD,WAAW,CACrCoD,GAA8B,IAAc;IAC3C,QAAQA,GAAG,CAACL,MAAM;MAChB,KAAK,aAAa;QAChBR,UAAU,CAAC,CAAC;QACZ,OAAO,IAAI;MACb,KAAK,cAAc;QACjBE,YAAY,CAAC,CAAC;QACd,OAAO,IAAI;MACb,KAAK,OAAO;QACV,IAAIW,GAAG,CAACC,EAAE,KAAK,QAAQ,EAAE;UACvBX,OAAO,CAAC,QAAQ,EAAEU,GAAG,CAACR,OAAO,EAAEQ,GAAG,CAACP,IAAI,CAAC;UACxC,OAAO,IAAI;QACb;QACA,IAAIO,GAAG,CAACC,EAAE,KAAK,QAAQ,EAAE;UACvBX,OAAO,CAAC,QAAQ,EAAEU,GAAG,CAACR,OAAO,EAAEQ,GAAG,CAACP,IAAI,CAAC;UACxC,OAAO,IAAI;QACb;QACA,OAAO,KAAK;MACd;QACE,OAAO,KAAK;IAChB;EACF,CAAC,EACD,CAACJ,YAAY,EAAEF,UAAU,EAAEG,OAAO,CACpC,CAAC;EAED,MAAMY,mBAAmB,GAAGtD,WAAW,CACpCuD,GAAW,IAAc;IACxB,IAAI,CAACA,GAAG,IAAI,OAAOA,GAAG,KAAK,QAAQ,EAAE;MACnC,OAAO,KAAK;IACd;IACA,IAAI;MACF,MAAMC,IAAI,GAAGpC,IAAI,CAACqC,KAAK,CAACF,GAAG,CAAC;MAC5B,IACEC,IAAI,EAAET,MAAM,KAAK,oBAAoB,IACrCS,IAAI,EAAEE,IAAI,KAAK,aAAa,IAC5BF,IAAI,EAAEjB,UAAU,KAAK,IAAI,EACzB;QACAA,UAAU,CAAC,CAAC;QACZ,OAAO,IAAI;MACb;IACF,CAAC,CAAC,MAAM;MACN;IAAA;IAEF,IAAItB,0BAA0B,CAAC0C,IAAI,CAAEC,CAAC,IAAKL,GAAG,CAACM,QAAQ,CAACD,CAAC,CAAC,CAAC,EAAE;MAC3DrB,UAAU,CAAC,CAAC;MACZ,OAAO,IAAI;IACb;IACA,OAAO,KAAK;EACd,CAAC,EACD,CAACA,UAAU,CACb,CAAC;EAED,MAAMuB,eAAe,GAAG9D,WAAW,CAChC+D,CAAoC,IAAK;IACxC,MAAMR,GAAG,GAAGQ,CAAC,EAAEC,WAAW,EAAER,IAAI,IAAI,EAAE;IACtC,MAAMJ,GAAG,GAAG3C,wBAAwB,CAAC8C,GAAG,CAAC;IAEzC,IAAIH,GAAG,IAAIA,GAAG,CAACP,IAAI,KAAK,QAAQ,IAAIM,oBAAoB,CAACC,GAAG,CAAC,EAAE;MAC7D;IACF;IACA,IAAIE,mBAAmB,CAACC,GAAG,CAAC,EAAE;MAC5B;IACF;IAEA,MAAMU,QAAQ,GAAIT,IAAa,IAAK;MAClC,MAAMrC,MAAM,GAAG;QACb2B,MAAM,EAAE,cAAc;QACtBD,IAAI,EAAE,QAAQ;QACdE,MAAM,EAAE,OAAO;QACfH,OAAO,EAAEY;MACX,CAAC;MACDzB,SAAS,CAACkB,OAAO,EAAEC,gBAAgB,CACjChC,6BAA6B,CAACC,MAAM,CACtC,CAAC;IACH,CAAC;IACDS,kBAAkB,GAAG2B,GAAG,EAAEU,QAAQ,CAAC;EACrC,CAAC,EACD,CAACd,oBAAoB,EAAEvB,kBAAkB,EAAE0B,mBAAmB,CAChE,CAAC;EAED,MAAMY,eAAe,GAAGlE,WAAW,CAChC+D,CAAoC,IAAK;IACxC,MAAMR,GAAG,GAAGQ,CAAC,EAAEC,WAAW,EAAER,IAAI,IAAI,EAAE;IACtC,MAAMJ,GAAG,GAAG3C,wBAAwB,CAAC8C,GAAG,CAAC;IAEzC,IAAIH,GAAG,IAAIA,GAAG,CAACP,IAAI,KAAK,QAAQ,IAAIM,oBAAoB,CAACC,GAAG,CAAC,EAAE;MAC7D;IACF;IACA,IAAIE,mBAAmB,CAACC,GAAG,CAAC,EAAE;MAC5B;IACF;EACF,CAAC,EACD,CAACJ,oBAAoB,EAAEG,mBAAmB,CAC5C,CAAC;EAED,MAAMa,uBAAuB,GAAGnE,WAAW,CACxCoE,OAAwB,IAAK;IAC5B,MAAM;MAAEC;IAAI,CAAC,GAAGD,OAAO;IACvB,IAAIC,GAAG,CAACC,UAAU,CAACtD,gBAAgB,CAAC,IAAIqD,GAAG,KAAK,qBAAqB,EAAE;MACrE9B,UAAU,CAAC,CAAC;MACZ,OAAO,KAAK;IACd;IACA,OAAO,IAAI;EACb,CAAC,EACD,CAACA,UAAU,CACb,CAAC;EAED,MAAMgC,YAAY,GAAGtE,OAAO,CAC1B,OAAO;IAAEuE,GAAG,EAAEhD;EAAgB,CAAC,CAAC,EAChC,CAACA,eAAe,CAClB,CAAC;EACD,MAAMiD,YAAY,GAAGxE,OAAO,CAAC,OAAO;IAAEuE,GAAG,EAAEjD;EAAa,CAAC,CAAC,EAAE,CAACA,YAAY,CAAC,CAAC;EAE3E,oBACEV,KAAA,CAACN,IAAI;IAACmE,KAAK,EAAEC,MAAM,CAACC,IAAK;IAAAC,QAAA,GACtB5C,OAAO,KAAK,QAAQ,gBACnBtB,IAAA,CAACH,OAAO;MAENwC,GAAG,EAAEjB,SAAU;MACf+C,MAAM,EAAEL,YAAa;MACrBM,eAAe,EAAE,CAAC,GAAG,CAAE;MACvBC,2BAA2B,EAAEjE,iBAAkB;MAC/CkE,SAAS,EAAEnB,eAAgB;MAC3BoB,4BAA4B,EAAEf,uBAAwB;MACtDgB,SAAS,EAAEtD,eAAgB;MAC3B6C,KAAK,EAAEC,MAAM,CAACS,OAAQ;MACtBC,oBAAoB;MACpBC,SAAS,EAAE,KAAM;MACjBC,uBAAuB,EAAEC;IAAQ,GAX5BrD,SAYN,CAAC,GACA,IAAI,EAEPF,OAAO,KAAK,QAAQ,gBACnBpB,KAAA,CAACN,IAAI;MAACmE,KAAK,EAAEC,MAAM,CAACc,YAAa;MAAAZ,QAAA,gBAC/BhE,KAAA,CAACN,IAAI;QAACmE,KAAK,EAAEC,MAAM,CAACe,MAAO;QAAAb,QAAA,gBACzBlE,IAAA,CAACL,gBAAgB;UACfqF,OAAO,EAAElD,YAAa;UACtBmD,OAAO,EAAE,EAAG;UACZC,iBAAiB,EAAC,QAAQ;UAC1BC,kBAAkB,EAAC,MAAM;UACzBpB,KAAK,EAAEC,MAAM,CAACoB,UAAW;UAAAlB,QAAA,EAExBnD,oBAAoB,GACnBA,oBAAoB,CAAC,CAAC,gBAEtBb,KAAA,CAACN,IAAI;YAACmE,KAAK,EAAEC,MAAM,CAACqB,eAAgB;YAAAnB,QAAA,gBAClClE,IAAA,CAACJ,IAAI;cAACmE,KAAK,EAAEC,MAAM,CAACsB;YAAc,CAAE,CAAC,eACrCtF,IAAA,CAACJ,IAAI;cAACmE,KAAK,EAAEC,MAAM,CAACuB;YAAa,CAAE,CAAC;UAAA,CAChC;QACP,CACe,CAAC,eAEnBvF,IAAA,CAACJ,IAAI;UAACmE,KAAK,EAAEC,MAAM,CAACwB;QAAa,CAAE,CAAC,EAEnC1E,WAAW,gBACVd,IAAA,CAACP,KAAK;UACJ0E,MAAM,EAAE;YAAEN,GAAG,EAAE/C;UAAY,CAAE;UAC7B2E,UAAU,EAAC,SAAS;UACpB1B,KAAK,EAAEC,MAAM,CAAC0B;QAAK,CACpB,CAAC,gBAEF1F,IAAA,CAACJ,IAAI;UAACmE,KAAK,EAAEC,MAAM,CAAC2B;QAAgB,CAAE,CACvC;MAAA,CACG,CAAC,eACP3F,IAAA,CAACH,OAAO;QAENwC,GAAG,EAAEhB,SAAU;QACf8C,MAAM,EAAEP,YAAa;QACrBQ,eAAe,EAAE,CAAC,GAAG,CAAE;QACvBC,2BAA2B,EAAEjE,iBAAkB;QAC/CkE,SAAS,EAAEf,eAAgB;QAC3BiB,SAAS,EAAErD,eAAgB;QAC3B4C,KAAK,EAAEC,MAAM,CAACS,OAAQ;QACtBC,oBAAoB;QACpBC,SAAS,EAAE,KAAM;QACjBC,uBAAuB,EAAEC;MAAQ,GAV5BnD,SAWN,CAAC;IAAA,CACE,CAAC,GACL,IAAI;EAAA,CACJ,CAAC;AAEX,CAAC;AAED,MAAMsC,MAAM,GAAGtE,UAAU,CAACkG,MAAM,CAAC;EAC/B3B,IAAI,EAAE;IAAE4B,IAAI,EAAE;EAAE,CAAC;EACjBf,YAAY,EAAE;IAAEe,IAAI,EAAE;EAAE,CAAC;EACzBpB,OAAO,EAAE;IAAEoB,IAAI,EAAE;EAAE,CAAC;EACpBd,MAAM,EAAE;IACNe,aAAa,EAAE,KAAK;IACpBC,UAAU,EAAE,QAAQ;IACpBC,eAAe,EAAE,MAAM;IACvBC,iBAAiB,EAAE,EAAE;IACrBC,UAAU,EAAE,EAAE;IACdC,aAAa,EAAE,EAAE;IACjBC,iBAAiB,EAAE1G,UAAU,CAAC2G,aAAa;IAC3CC,iBAAiB,EAAE;EACrB,CAAC;EACDlB,UAAU,EAAE;IACVmB,KAAK,EAAE,EAAE;IACTC,MAAM,EAAE,EAAE;IACVC,cAAc,EAAE,QAAQ;IACxBV,UAAU,EAAE;EACd,CAAC;EACDV,eAAe,EAAE;IACfkB,KAAK,EAAE,EAAE;IACTC,MAAM,EAAE,EAAE;IACVC,cAAc,EAAE,QAAQ;IACxBC,QAAQ,EAAE;EACZ,CAAC;EACDpB,aAAa,EAAE;IACboB,QAAQ,EAAE,UAAU;IACpBC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRJ,MAAM,EAAE,CAAC;IACTR,eAAe,EAAE,SAAS;IAC1Ba,YAAY,EAAE;EAChB,CAAC;EACDtB,YAAY,EAAE;IACZgB,KAAK,EAAE,EAAE;IACTC,MAAM,EAAE,EAAE;IACVM,eAAe,EAAE,CAAC;IAClBV,iBAAiB,EAAE,CAAC;IACpBW,WAAW,EAAE,SAAS;IACtBC,SAAS,EAAE,CAAC;MAAEC,MAAM,EAAE;IAAQ,CAAC,CAAC;IAChCC,UAAU,EAAE;EACd,CAAC;EACD1B,YAAY,EAAE;IACZK,IAAI,EAAE;EACR,CAAC;EACDH,IAAI,EAAE;IACJa,KAAK,EAAE,GAAG;IACVC,MAAM,EAAE;EACV,CAAC;EACDb,eAAe,EAAE;IACfY,KAAK,EAAE,GAAG;IACVC,MAAM,EAAE;EACV;AACF,CAAC,CAAC;AAEF,eAAe7F,2BAA2B","ignoreList":[]}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Messages posted from either WebView via ReactNativeWebView.postMessage(string).
5
+ * Pages should JSON.stringify envelopes before posting.
6
+ */
7
+
8
+ export function parseDualWebViewEnvelope(raw) {
9
+ try {
10
+ const o = JSON.parse(raw);
11
+ if (o?.bridge === 'dual-webview' && o?.from && o?.action) {
12
+ return o;
13
+ }
14
+ } catch {
15
+ /* not JSON */
16
+ }
17
+ return null;
18
+ }
19
+ //# sourceMappingURL=dualWebViewBridge.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["parseDualWebViewEnvelope","raw","o","JSON","parse","bridge","from","action"],"sourceRoot":"../../../../src","sources":["services/webview/dualWebViewBridge.types.ts"],"mappings":";;AAAA;AACA;AACA;AACA;;AAgBA,OAAO,SAASA,wBAAwBA,CACtCC,GAAW,EACuB;EAClC,IAAI;IACF,MAAMC,CAAC,GAAGC,IAAI,CAACC,KAAK,CAACH,GAAG,CAA8B;IACtD,IAAIC,CAAC,EAAEG,MAAM,KAAK,cAAc,IAAIH,CAAC,EAAEI,IAAI,IAAIJ,CAAC,EAAEK,MAAM,EAAE;MACxD,OAAOL,CAAC;IACV;EACF,CAAC,CAAC,MAAM;IACN;EAAA;EAEF,OAAO,IAAI;AACb","ignoreList":[]}
@@ -5,6 +5,8 @@
5
5
  // Default environment (build-time injected)
6
6
  const injectedEnv = 'sandbox';
7
7
  const DEFAULT_ENVIRONMENT = injectedEnv === 'production' ? 'production' : 'sandbox';
8
+
9
+ /** Hosted merchant web origins; aligned with `SaversSdkHostedEnvironment` via `setEnvironment` in SDK init. */
8
10
  const ENV_URLS = {
9
11
  sandbox: 'https://testm.saversapp.com/',
10
12
  production: 'https://m.saversapp.com/'
@@ -1 +1 @@
1
- {"version":3,"names":["injectedEnv","DEFAULT_ENVIRONMENT","ENV_URLS","sandbox","production","currentEnvironment","BASE_URL","setEnvironment","env","getEnvironment","getBaseUrl"],"sourceRoot":"../../../src","sources":["utils/config.ts"],"mappings":";;AAAA;;AAIA;AACA,MAAMA,WAAW,GAAG,SAAmB;AACvC,MAAMC,mBAAgC,GACpCD,WAAW,KAAK,YAAY,GAAG,YAAY,GAAG,SAAS;AAEzD,MAAME,QAAqC,GAAG;EAC5CC,OAAO,EAAE,8BAA8B;EACvCC,UAAU,EAAE;AACd,CAAC;;AAED;AACA,IAAIC,kBAA+B,GAAGJ,mBAAmB;;AAEzD;AACA,IAAIK,QAAgB,GAAGJ,QAAQ,CAACG,kBAAkB,CAAC;;AAEnD;AACA,OAAO,MAAME,cAAc,GAAIC,GAAgB,IAAK;EAClDH,kBAAkB,GAAGG,GAAG;EACxBF,QAAQ,GAAGJ,QAAQ,CAACM,GAAG,CAAC;AAC1B,CAAC;;AAED;AACA,OAAO,MAAMC,cAAc,GAAGA,CAAA,KAAmB;EAC/C,OAAOJ,kBAAkB;AAC3B,CAAC;;AAED;AACA,OAAO,MAAMK,UAAU,GAAGA,CAAA,KAAc;EACtC,OAAOJ,QAAQ;AACjB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["injectedEnv","DEFAULT_ENVIRONMENT","ENV_URLS","sandbox","production","currentEnvironment","BASE_URL","setEnvironment","env","getEnvironment","getBaseUrl"],"sourceRoot":"../../../src","sources":["utils/config.ts"],"mappings":";;AAAA;;AAIA;AACA,MAAMA,WAAW,GAAG,SAAmB;AACvC,MAAMC,mBAAgC,GACpCD,WAAW,KAAK,YAAY,GAAG,YAAY,GAAG,SAAS;;AAEzD;AACA,MAAME,QAAqC,GAAG;EAC5CC,OAAO,EAAE,8BAA8B;EACvCC,UAAU,EAAE;AACd,CAAC;;AAED;AACA,IAAIC,kBAA+B,GAAGJ,mBAAmB;;AAEzD;AACA,IAAIK,QAAgB,GAAGJ,QAAQ,CAACG,kBAAkB,CAAC;;AAEnD;AACA,OAAO,MAAME,cAAc,GAAIC,GAAgB,IAAK;EAClDH,kBAAkB,GAAGG,GAAG;EACxBF,QAAQ,GAAGJ,QAAQ,CAACM,GAAG,CAAC;AAC1B,CAAC;;AAED;AACA,OAAO,MAAMC,cAAc,GAAGA,CAAA,KAAmB;EAC/C,OAAOJ,kBAAkB;AAC3B,CAAC;;AAED;AACA,OAAO,MAAMK,UAAU,GAAGA,CAAA,KAAc;EACtC,OAAOJ,QAAQ;AACjB,CAAC","ignoreList":[]}
@@ -1,9 +1,13 @@
1
+ /** Controls which hosted merchant web origin `generateUrl` uses. */
2
+ export type SaversSdkHostedEnvironment = 'SANDBOX' | 'PROD';
1
3
  export declare function initializeSDK(providedKeys: {
2
4
  apiKey: string;
3
5
  encryptionKey: string;
4
6
  pRefCode: string;
5
7
  authMode: string;
6
8
  navigationRef: any;
9
+ /** When set, `generateUrl` uses testm (SANDBOX) or m (PROD). When omitted, build-time default from config applies. */
10
+ environment?: SaversSdkHostedEnvironment;
7
11
  }): void;
8
12
  export declare const SaversAppSDK: {
9
13
  initialized: typeof initializeSDK;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../../src/core/runtime.ts"],"names":[],"mappings":"AAiBA,wBAAgB,aAAa,CAAC,YAAY,EAAE;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,GAAG,CAAC;CACpB,QAwDA;AAED,eAAO,MAAM,YAAY;;CAExB,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../../src/core/runtime.ts"],"names":[],"mappings":"AAoBA,oEAAoE;AACpE,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,MAAM,CAAC;AAE5D,wBAAgB,aAAa,CAAC,YAAY,EAAE;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,GAAG,CAAC;IACnB,sHAAsH;IACtH,WAAW,CAAC,EAAE,0BAA0B,CAAC;CAC1C,QAqEA;AAED,eAAO,MAAM,YAAY;;CAExB,CAAC"}
@@ -6,6 +6,8 @@ export * from './utils/errors';
6
6
  export * from './utils/logger';
7
7
  export * from './utils/validator';
8
8
  export * from './services/webview/messageHandler';
9
+ export * from './services/webview/DualWebViewBridgeController';
10
+ export * from './services/webview/dualWebViewBridge.types';
9
11
  export * from './data/storage/deviceIdManager';
10
12
  export * from './services/navigation/dialPad';
11
13
  export * from './services/navigation/openBrowser';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mCAAmC,CAAC;AAClD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wCAAwC,CAAC;AACvD,cAAc,0CAA0C,CAAC;AACzD,cAAc,yBAAyB,CAAC;AACxC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mCAAmC,CAAC;AAClD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ export type Surface = 'savers' | 'travel';
3
+ export type DualWebViewBridgeControllerProps = {
4
+ saversAppUrl: string;
5
+ travelPortalUrl?: string;
6
+ partnerLogo?: string;
7
+ renderTravelBackIcon?: () => React.ReactNode;
8
+ initialSurface?: Surface;
9
+ onSaversSdkMessage?: (raw: string, postBack: (data: unknown) => void) => void;
10
+ onLoadEndSavers?: () => void;
11
+ onLoadEndTravel?: () => void;
12
+ };
13
+ export declare const DualWebViewBridgeController: React.FC<DualWebViewBridgeControllerProps>;
14
+ export default DualWebViewBridgeController;
15
+ //# sourceMappingURL=DualWebViewBridgeController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DualWebViewBridgeController.d.ts","sourceRoot":"","sources":["../../../../../src/services/webview/DualWebViewBridgeController.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAiBtE,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1C,MAAM,MAAM,gCAAgC,GAAG;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC7C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;IAC9E,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B,CAAC;AAcF,eAAO,MAAM,2BAA2B,EAAE,KAAK,CAAC,EAAE,CAChD,gCAAgC,CAoOjC,CAAC;AA0DF,eAAe,2BAA2B,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Messages posted from either WebView via ReactNativeWebView.postMessage(string).
3
+ * Pages should JSON.stringify envelopes before posting.
4
+ */
5
+ export type DualWebViewBridgeEnvelope = {
6
+ bridge: 'dual-webview';
7
+ /** Who sent this message (set by the page). */
8
+ from: 'savers' | 'travel';
9
+ /**
10
+ * relay — forward payload to the other WebView via injectJavaScript + CustomEvent.
11
+ * open_travel — show travel surface (reloads travel WebView).
12
+ * close_travel — same as native back (reloads Savers WebView).
13
+ */
14
+ action: 'relay' | 'open_travel' | 'close_travel';
15
+ /** For relay: which surface should receive the event. */
16
+ to?: 'savers' | 'travel';
17
+ payload?: unknown;
18
+ };
19
+ export declare function parseDualWebViewEnvelope(raw: string): DualWebViewBridgeEnvelope | null;
20
+ //# sourceMappingURL=dualWebViewBridge.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dualWebViewBridge.types.d.ts","sourceRoot":"","sources":["../../../../../src/services/webview/dualWebViewBridge.types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,cAAc,CAAC;IACvB,+CAA+C;IAC/C,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B;;;;OAIG;IACH,MAAM,EAAE,OAAO,GAAG,aAAa,GAAG,cAAc,CAAC;IACjD,yDAAyD;IACzD,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,GACV,yBAAyB,GAAG,IAAI,CAUlC"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/utils/config.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,YAAY,CAAC;AAmBnD,eAAO,MAAM,cAAc,GAAI,KAAK,WAAW,SAG9C,CAAC;AAGF,eAAO,MAAM,cAAc,QAAO,WAEjC,CAAC;AAGF,eAAO,MAAM,UAAU,QAAO,MAE7B,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/utils/config.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,YAAY,CAAC;AAoBnD,eAAO,MAAM,cAAc,GAAI,KAAK,WAAW,SAG9C,CAAC;AAGF,eAAO,MAAM,cAAc,QAAO,WAEjC,CAAC;AAGF,eAAO,MAAM,UAAU,QAAO,MAE7B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savers_app/react-native-sandbox-sdk",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "Cross-platform React Native SDK exposing native features (maps, dial pad, browser), device ID/location and session utilities, a URL generator, and a WebView message bridge to trigger actions from web content.",
5
5
  "main": "./lib/module/index.js",
6
6
  "react-native": "./src/index.tsx",
@@ -93,6 +93,8 @@
93
93
  "react": "19.1.0",
94
94
  "react-native": "0.81.5",
95
95
  "react-native-builder-bob": "^0.40.13",
96
+ "react-native-safe-area-context": "^5.5.2",
97
+ "react-native-webview": "^13.16.0",
96
98
  "release-it": "^19.0.4",
97
99
  "typescript": "^5.9.2"
98
100
  },
@@ -104,7 +106,9 @@
104
106
  "react": "*",
105
107
  "react-native": "*",
106
108
  "react-native-aes-gcm-crypto": "*",
107
- "react-native-device-info": "*"
109
+ "react-native-device-info": "*",
110
+ "react-native-safe-area-context": "*",
111
+ "react-native-webview": "*"
108
112
  },
109
113
  "workspaces": [
110
114
  "example",
@@ -12,15 +12,23 @@ import {
12
12
  import { getDeviceId } from '../data/storage/deviceIdManager';
13
13
  import { setReactRef } from '../data/storage/reactRefManager';
14
14
 
15
+ // Hosted app base URL (test vs production merchant web)
16
+ import { setEnvironment } from '../utils/config';
17
+
15
18
  // Services
16
19
  // import { getDeviceLocation } from '../services/device/location';
17
20
 
21
+ /** Controls which hosted merchant web origin `generateUrl` uses. */
22
+ export type SaversSdkHostedEnvironment = 'SANDBOX' | 'PROD';
23
+
18
24
  export function initializeSDK(providedKeys: {
19
25
  apiKey: string;
20
26
  encryptionKey: string;
21
27
  pRefCode: string;
22
28
  authMode: string;
23
29
  navigationRef: any;
30
+ /** When set, `generateUrl` uses testm (SANDBOX) or m (PROD). When omitted, build-time default from config applies. */
31
+ environment?: SaversSdkHostedEnvironment;
24
32
  }) {
25
33
  const keys = providedKeys;
26
34
  const missing: string[] = [];
@@ -35,6 +43,19 @@ export function initializeSDK(providedKeys: {
35
43
  logger.warn(msg);
36
44
  throw new Error(msg);
37
45
  }
46
+
47
+ if (keys.environment !== undefined) {
48
+ if (keys.environment !== 'SANDBOX' && keys.environment !== 'PROD') {
49
+ throw new Error(
50
+ `[SDK] Invalid environment: ${String(keys.environment)}. Use 'SANDBOX' or 'PROD'.`
51
+ );
52
+ }
53
+ setEnvironment(keys.environment === 'SANDBOX' ? 'sandbox' : 'production');
54
+ logger.info('[SDK] Hosted app environment', {
55
+ environment: keys.environment,
56
+ });
57
+ }
58
+
38
59
  setApiKey(keys.apiKey);
39
60
  setEncryptionKey(keys.encryptionKey);
40
61
  setPRefCode(keys.pRefCode);
package/src/index.tsx CHANGED
@@ -6,6 +6,8 @@ export * from './utils/errors';
6
6
  export * from './utils/logger';
7
7
  export * from './utils/validator';
8
8
  export * from './services/webview/messageHandler';
9
+ export * from './services/webview/DualWebViewBridgeController';
10
+ export * from './services/webview/dualWebViewBridge.types';
9
11
  export * from './data/storage/deviceIdManager';
10
12
  export * from './services/navigation/dialPad';
11
13
  export * from './services/navigation/openBrowser';
@@ -0,0 +1,330 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { Image, StyleSheet, TouchableOpacity, View } from 'react-native';
3
+ import WebView from 'react-native-webview';
4
+ import {
5
+ type DualWebViewBridgeEnvelope,
6
+ parseDualWebViewEnvelope,
7
+ } from './dualWebViewBridge.types';
8
+
9
+ const DEFAULT_TRAVEL_PORTAL_URL = 'https://sandbox.travelercashback.com';
10
+
11
+ /** Must match `applicationNameForUserAgent` on clo-app WebViews (DualWebViewBridgeController). */
12
+ const WEBVIEW_UA_MARKER = 'CloAppWebView';
13
+
14
+ /** Legacy / Hub: open travel from first WebView without JSON envelope. */
15
+ const TRAVEL_DEEP_LINK = 'saversapp://travel';
16
+ const LEGACY_OPEN_TRAVEL_ACTIONS = ['OPEN_TRAVEL_PORTAL', 'OPEN_TRAVEL'];
17
+
18
+ export type Surface = 'savers' | 'travel';
19
+
20
+ export type DualWebViewBridgeControllerProps = {
21
+ saversAppUrl: string;
22
+ travelPortalUrl?: string;
23
+ partnerLogo?: string;
24
+ renderTravelBackIcon?: () => React.ReactNode;
25
+ initialSurface?: Surface;
26
+ onSaversSdkMessage?: (raw: string, postBack: (data: unknown) => void) => void;
27
+ onLoadEndSavers?: () => void;
28
+ onLoadEndTravel?: () => void;
29
+ };
30
+
31
+ function buildInjectNativeBridgeScript(
32
+ detail: Record<string, unknown>
33
+ ): string {
34
+ return `
35
+ (function(){
36
+ var detail = ${JSON.stringify(detail)};
37
+ window.dispatchEvent(new CustomEvent('nativeBridge', { detail: detail }));
38
+ true;
39
+ })();
40
+ `;
41
+ }
42
+
43
+ export const DualWebViewBridgeController: React.FC<
44
+ DualWebViewBridgeControllerProps
45
+ > = ({
46
+ saversAppUrl,
47
+ travelPortalUrl = DEFAULT_TRAVEL_PORTAL_URL,
48
+ partnerLogo,
49
+ renderTravelBackIcon,
50
+ initialSurface = 'savers',
51
+ onSaversSdkMessage,
52
+ onLoadEndSavers,
53
+ onLoadEndTravel,
54
+ }) => {
55
+ const saversRef = useRef<WebView>(null);
56
+ const travelRef = useRef<WebView>(null);
57
+
58
+ const [surface, setSurface] = useState<Surface>(initialSurface);
59
+ const [saversKey, setSaversKey] = useState(0);
60
+ const [travelKey, setTravelKey] = useState(0);
61
+
62
+ const openTravel = useCallback(() => {
63
+ setSurface('travel');
64
+ setTravelKey((k) => k + 1);
65
+ }, []);
66
+
67
+ const backToSavers = useCallback(() => {
68
+ setSurface('savers');
69
+ setSaversKey((k) => k + 1);
70
+ }, []);
71
+
72
+ const relayTo = useCallback(
73
+ (
74
+ target: 'savers' | 'travel',
75
+ payload: unknown,
76
+ from: 'savers' | 'travel'
77
+ ) => {
78
+ const detail = {
79
+ bridge: 'dual-webview',
80
+ from,
81
+ action: 'relay' as const,
82
+ payload,
83
+ };
84
+ const ref = target === 'savers' ? saversRef : travelRef;
85
+ ref.current?.injectJavaScript(buildInjectNativeBridgeScript(detail));
86
+ },
87
+ []
88
+ );
89
+
90
+ const handleBridgeEnvelope = useCallback(
91
+ (env: DualWebViewBridgeEnvelope): boolean => {
92
+ switch (env.action) {
93
+ case 'open_travel':
94
+ openTravel();
95
+ return true;
96
+ case 'close_travel':
97
+ backToSavers();
98
+ return true;
99
+ case 'relay':
100
+ if (env.to === 'savers') {
101
+ relayTo('savers', env.payload, env.from);
102
+ return true;
103
+ }
104
+ if (env.to === 'travel') {
105
+ relayTo('travel', env.payload, env.from);
106
+ return true;
107
+ }
108
+ return false;
109
+ default:
110
+ return false;
111
+ }
112
+ },
113
+ [backToSavers, openTravel, relayTo]
114
+ );
115
+
116
+ const tryLegacyOpenTravel = useCallback(
117
+ (raw: string): boolean => {
118
+ if (!raw || typeof raw !== 'string') {
119
+ return false;
120
+ }
121
+ try {
122
+ const data = JSON.parse(raw);
123
+ if (
124
+ data?.action === 'OPEN_TRAVEL_PORTAL' ||
125
+ data?.type === 'OPEN_TRAVEL' ||
126
+ data?.openTravel === true
127
+ ) {
128
+ openTravel();
129
+ return true;
130
+ }
131
+ } catch {
132
+ /* ignore */
133
+ }
134
+ if (LEGACY_OPEN_TRAVEL_ACTIONS.some((s) => raw.includes(s))) {
135
+ openTravel();
136
+ return true;
137
+ }
138
+ return false;
139
+ },
140
+ [openTravel]
141
+ );
142
+
143
+ const onMessageSavers = useCallback(
144
+ (e: { nativeEvent: { data: string } }) => {
145
+ const raw = e?.nativeEvent?.data ?? '';
146
+ const env = parseDualWebViewEnvelope(raw);
147
+
148
+ if (env && env.from === 'savers' && handleBridgeEnvelope(env)) {
149
+ return;
150
+ }
151
+ if (tryLegacyOpenTravel(raw)) {
152
+ return;
153
+ }
154
+
155
+ const postBack = (data: unknown) => {
156
+ const detail = {
157
+ bridge: 'dual-webview',
158
+ from: 'native',
159
+ action: 'relay',
160
+ payload: data,
161
+ };
162
+ saversRef.current?.injectJavaScript(
163
+ buildInjectNativeBridgeScript(detail)
164
+ );
165
+ };
166
+ onSaversSdkMessage?.(raw, postBack);
167
+ },
168
+ [handleBridgeEnvelope, onSaversSdkMessage, tryLegacyOpenTravel]
169
+ );
170
+
171
+ const onMessageTravel = useCallback(
172
+ (e: { nativeEvent: { data: string } }) => {
173
+ const raw = e?.nativeEvent?.data ?? '';
174
+ const env = parseDualWebViewEnvelope(raw);
175
+
176
+ if (env && env.from === 'travel' && handleBridgeEnvelope(env)) {
177
+ return;
178
+ }
179
+ if (tryLegacyOpenTravel(raw)) {
180
+ return;
181
+ }
182
+ },
183
+ [handleBridgeEnvelope, tryLegacyOpenTravel]
184
+ );
185
+
186
+ const onShouldStartLoadSavers = useCallback(
187
+ (request: { url: string }) => {
188
+ const { url } = request;
189
+ if (url.startsWith(TRAVEL_DEEP_LINK) || url === 'saversapp://travel/') {
190
+ openTravel();
191
+ return false;
192
+ }
193
+ return true;
194
+ },
195
+ [openTravel]
196
+ );
197
+
198
+ const travelSource = useMemo(
199
+ () => ({ uri: travelPortalUrl }),
200
+ [travelPortalUrl]
201
+ );
202
+ const saversSource = useMemo(() => ({ uri: saversAppUrl }), [saversAppUrl]);
203
+
204
+ return (
205
+ <View style={styles.root}>
206
+ {surface === 'savers' ? (
207
+ <WebView
208
+ key={saversKey}
209
+ ref={saversRef}
210
+ source={saversSource}
211
+ originWhitelist={['*']}
212
+ applicationNameForUserAgent={WEBVIEW_UA_MARKER}
213
+ onMessage={onMessageSavers}
214
+ onShouldStartLoadWithRequest={onShouldStartLoadSavers}
215
+ onLoadEnd={onLoadEndSavers}
216
+ style={styles.webview}
217
+ sharedCookiesEnabled
218
+ incognito={false}
219
+ webviewDebuggingEnabled={__DEV__}
220
+ />
221
+ ) : null}
222
+
223
+ {surface === 'travel' ? (
224
+ <View style={styles.travelColumn}>
225
+ <View style={styles.header}>
226
+ <TouchableOpacity
227
+ onPress={backToSavers}
228
+ hitSlop={16}
229
+ accessibilityRole="button"
230
+ accessibilityLabel="Back"
231
+ style={styles.backButton}
232
+ >
233
+ {renderTravelBackIcon ? (
234
+ renderTravelBackIcon()
235
+ ) : (
236
+ <View style={styles.backIconWrapper}>
237
+ <View style={styles.backIconShaft} />
238
+ <View style={styles.backIconHead} />
239
+ </View>
240
+ )}
241
+ </TouchableOpacity>
242
+
243
+ <View style={styles.headerSpacer} />
244
+
245
+ {partnerLogo ? (
246
+ <Image
247
+ source={{ uri: partnerLogo }}
248
+ resizeMode="contain"
249
+ style={styles.logo}
250
+ />
251
+ ) : (
252
+ <View style={styles.logoPlaceholder} />
253
+ )}
254
+ </View>
255
+ <WebView
256
+ key={travelKey}
257
+ ref={travelRef}
258
+ source={travelSource}
259
+ originWhitelist={['*']}
260
+ applicationNameForUserAgent={WEBVIEW_UA_MARKER}
261
+ onMessage={onMessageTravel}
262
+ onLoadEnd={onLoadEndTravel}
263
+ style={styles.webview}
264
+ sharedCookiesEnabled
265
+ incognito={false}
266
+ webviewDebuggingEnabled={__DEV__}
267
+ />
268
+ </View>
269
+ ) : null}
270
+ </View>
271
+ );
272
+ };
273
+
274
+ const styles = StyleSheet.create({
275
+ root: { flex: 1 },
276
+ travelColumn: { flex: 1 },
277
+ webview: { flex: 1 },
278
+ header: {
279
+ flexDirection: 'row',
280
+ alignItems: 'center',
281
+ backgroundColor: '#fff',
282
+ paddingHorizontal: 16,
283
+ paddingTop: 12,
284
+ paddingBottom: 12,
285
+ borderBottomWidth: StyleSheet.hairlineWidth,
286
+ borderBottomColor: '#E5E5E5',
287
+ },
288
+ backButton: {
289
+ width: 32,
290
+ height: 32,
291
+ justifyContent: 'center',
292
+ alignItems: 'center',
293
+ },
294
+ backIconWrapper: {
295
+ width: 20,
296
+ height: 16,
297
+ justifyContent: 'center',
298
+ position: 'relative',
299
+ },
300
+ backIconShaft: {
301
+ position: 'absolute',
302
+ left: 6,
303
+ right: 0,
304
+ height: 2,
305
+ backgroundColor: '#111827',
306
+ borderRadius: 1,
307
+ },
308
+ backIconHead: {
309
+ width: 10,
310
+ height: 10,
311
+ borderLeftWidth: 2,
312
+ borderBottomWidth: 2,
313
+ borderColor: '#111827',
314
+ transform: [{ rotate: '45deg' }],
315
+ marginLeft: 1,
316
+ },
317
+ headerSpacer: {
318
+ flex: 1,
319
+ },
320
+ logo: {
321
+ width: 120,
322
+ height: 32,
323
+ },
324
+ logoPlaceholder: {
325
+ width: 120,
326
+ height: 32,
327
+ },
328
+ });
329
+
330
+ export default DualWebViewBridgeController;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Messages posted from either WebView via ReactNativeWebView.postMessage(string).
3
+ * Pages should JSON.stringify envelopes before posting.
4
+ */
5
+ export type DualWebViewBridgeEnvelope = {
6
+ bridge: 'dual-webview';
7
+ /** Who sent this message (set by the page). */
8
+ from: 'savers' | 'travel';
9
+ /**
10
+ * relay — forward payload to the other WebView via injectJavaScript + CustomEvent.
11
+ * open_travel — show travel surface (reloads travel WebView).
12
+ * close_travel — same as native back (reloads Savers WebView).
13
+ */
14
+ action: 'relay' | 'open_travel' | 'close_travel';
15
+ /** For relay: which surface should receive the event. */
16
+ to?: 'savers' | 'travel';
17
+ payload?: unknown;
18
+ };
19
+
20
+ export function parseDualWebViewEnvelope(
21
+ raw: string
22
+ ): DualWebViewBridgeEnvelope | null {
23
+ try {
24
+ const o = JSON.parse(raw) as DualWebViewBridgeEnvelope;
25
+ if (o?.bridge === 'dual-webview' && o?.from && o?.action) {
26
+ return o;
27
+ }
28
+ } catch {
29
+ /* not JSON */
30
+ }
31
+ return null;
32
+ }
@@ -7,6 +7,7 @@ const injectedEnv = 'sandbox' as string;
7
7
  const DEFAULT_ENVIRONMENT: Environment =
8
8
  injectedEnv === 'production' ? 'production' : 'sandbox';
9
9
 
10
+ /** Hosted merchant web origins; aligned with `SaversSdkHostedEnvironment` via `setEnvironment` in SDK init. */
10
11
  const ENV_URLS: Record<Environment, string> = {
11
12
  sandbox: 'https://testm.saversapp.com/',
12
13
  production: 'https://m.saversapp.com/',