@deuna/react-native-sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/lib/module/DeunaSDK.js +37 -14
  2. package/lib/module/DeunaSDK.js.map +1 -1
  3. package/lib/module/components/DeunaWebView.js +2 -1
  4. package/lib/module/components/DeunaWebView.js.map +1 -1
  5. package/lib/module/components/DeunaWidget.js +7 -2
  6. package/lib/module/components/DeunaWidget.js.map +1 -1
  7. package/lib/module/components/DeviceFingerprintWebView.js +44 -0
  8. package/lib/module/components/DeviceFingerprintWebView.js.map +1 -0
  9. package/lib/module/components/NewTabWebView.js +2 -1
  10. package/lib/module/components/NewTabWebView.js.map +1 -1
  11. package/lib/module/controllers/BaseWebViewController.js +59 -17
  12. package/lib/module/controllers/BaseWebViewController.js.map +1 -1
  13. package/lib/module/controllers/DeviceFingerprintController.js +69 -0
  14. package/lib/module/controllers/DeviceFingerprintController.js.map +1 -0
  15. package/lib/module/controllers/OpenInNewTabController.js +24 -8
  16. package/lib/module/controllers/OpenInNewTabController.js.map +1 -1
  17. package/lib/module/interfaces/constants.js +1 -0
  18. package/lib/module/interfaces/constants.js.map +1 -1
  19. package/lib/module/types/helpers/buildElementsLink.js +63 -48
  20. package/lib/module/types/helpers/buildElementsLink.js.map +1 -1
  21. package/lib/module/types/helpers/buildNextActionLink.js +7 -3
  22. package/lib/module/types/helpers/buildNextActionLink.js.map +1 -1
  23. package/lib/module/types/helpers/buildPaymentLink.js +1 -3
  24. package/lib/module/types/helpers/buildPaymentLink.js.map +1 -1
  25. package/lib/module/types/helpers/buildVoucherLink.js +1 -3
  26. package/lib/module/types/helpers/buildVoucherLink.js.map +1 -1
  27. package/lib/typescript/deuna-sdk-react-native/src/DeunaSDK.d.ts +9 -1
  28. package/lib/typescript/deuna-sdk-react-native/src/DeunaSDK.d.ts.map +1 -1
  29. package/lib/typescript/deuna-sdk-react-native/src/components/DeunaWebView.d.ts +1 -0
  30. package/lib/typescript/deuna-sdk-react-native/src/components/DeunaWebView.d.ts.map +1 -1
  31. package/lib/typescript/deuna-sdk-react-native/src/components/DeunaWidget.d.ts.map +1 -1
  32. package/lib/typescript/deuna-sdk-react-native/src/components/DeviceFingerprintWebView.d.ts +7 -0
  33. package/lib/typescript/deuna-sdk-react-native/src/components/DeviceFingerprintWebView.d.ts.map +1 -0
  34. package/lib/typescript/deuna-sdk-react-native/src/components/NewTabWebView.d.ts.map +1 -1
  35. package/lib/typescript/deuna-sdk-react-native/src/controllers/BaseWebViewController.d.ts +26 -4
  36. package/lib/typescript/deuna-sdk-react-native/src/controllers/BaseWebViewController.d.ts.map +1 -1
  37. package/lib/typescript/deuna-sdk-react-native/src/controllers/DeviceFingerprintController.d.ts +15 -0
  38. package/lib/typescript/deuna-sdk-react-native/src/controllers/DeviceFingerprintController.d.ts.map +1 -0
  39. package/lib/typescript/deuna-sdk-react-native/src/controllers/OpenInNewTabController.d.ts +2 -2
  40. package/lib/typescript/deuna-sdk-react-native/src/controllers/OpenInNewTabController.d.ts.map +1 -1
  41. package/lib/typescript/deuna-sdk-react-native/src/interfaces/constants.d.ts +1 -0
  42. package/lib/typescript/deuna-sdk-react-native/src/interfaces/constants.d.ts.map +1 -1
  43. package/lib/typescript/deuna-sdk-react-native/src/types/helpers/buildElementsLink.d.ts +5 -0
  44. package/lib/typescript/deuna-sdk-react-native/src/types/helpers/buildElementsLink.d.ts.map +1 -1
  45. package/lib/typescript/deuna-sdk-react-native/src/types/helpers/buildNextActionLink.d.ts.map +1 -1
  46. package/lib/typescript/deuna-sdk-react-native/src/types/helpers/buildPaymentLink.d.ts.map +1 -1
  47. package/lib/typescript/deuna-sdk-react-native/src/types/helpers/buildVoucherLink.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/DeunaSDK.ts +42 -14
  50. package/src/components/DeunaWebView.tsx +2 -0
  51. package/src/components/DeunaWidget.tsx +9 -0
  52. package/src/components/DeviceFingerprintWebView.tsx +58 -0
  53. package/src/components/NewTabWebView.tsx +3 -0
  54. package/src/controllers/BaseWebViewController.ts +76 -19
  55. package/src/controllers/DeviceFingerprintController.ts +86 -0
  56. package/src/controllers/OpenInNewTabController.ts +30 -13
  57. package/src/interfaces/constants.ts +3 -0
  58. package/src/types/helpers/buildElementsLink.ts +89 -56
  59. package/src/types/helpers/buildNextActionLink.ts +8 -4
  60. package/src/types/helpers/buildPaymentLink.ts +1 -4
  61. package/src/types/helpers/buildVoucherLink.ts +1 -4
@@ -8,6 +8,7 @@ interface DeunaWebViewProps {
8
8
  onMessage?: (event: any) => void;
9
9
  onLoad?: () => void;
10
10
  onError?: (error: any) => void;
11
+ onShouldStartLoadWithRequest?: (request: any) => boolean;
11
12
  }
12
13
 
13
14
  export const DeunaWebView = (props: DeunaWebViewProps) => {
@@ -30,6 +31,7 @@ export const DeunaWebView = (props: DeunaWebViewProps) => {
30
31
  onError={props.onError}
31
32
  setSupportMultipleWindows={false}
32
33
  javaScriptCanOpenWindowsAutomatically={false}
34
+ onShouldStartLoadWithRequest={props.onShouldStartLoadWithRequest}
33
35
  />
34
36
  </View>
35
37
  );
@@ -6,6 +6,7 @@ import { DeunaWebView } from './DeunaWebView';
6
6
  import { NewTabWebView } from './NewTabWebView';
7
7
  import { DeunaWebViewController } from '../controllers/BaseWebViewController';
8
8
  import { Mode } from '../interfaces/types';
9
+ import { DeviceFingerprintWebView } from './DeviceFingerprintWebView';
9
10
 
10
11
  interface DeunaWidgetProps {
11
12
  instance: DeunaSDK;
@@ -67,6 +68,10 @@ export const DeunaWidget = (props: DeunaWidgetProps) => {
67
68
  onMessage={instanceRef.current.webViewController?.onMessage}
68
69
  onLoad={instanceRef.current.webViewController?.onLoad}
69
70
  onError={instanceRef.current.webViewController?.onError}
71
+ onShouldStartLoadWithRequest={
72
+ instanceRef.current.webViewController
73
+ ?.onShouldStartLoadWithRequest
74
+ }
70
75
  />
71
76
  <NewTabWebView instance={instanceRef.current} />
72
77
  </View>
@@ -83,8 +88,12 @@ export const DeunaWidget = (props: DeunaWidgetProps) => {
83
88
  onMessage={instanceRef.current.webViewController?.onMessage}
84
89
  onLoad={instanceRef.current.webViewController?.onLoad}
85
90
  onError={instanceRef.current.webViewController?.onError}
91
+ onShouldStartLoadWithRequest={
92
+ instanceRef.current.webViewController?.onShouldStartLoadWithRequest
93
+ }
86
94
  />
87
95
  )}
96
+ <DeviceFingerprintWebView instance={instanceRef.current} />
88
97
  </>
89
98
  );
90
99
  };
@@ -0,0 +1,58 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { DeunaSDK } from '../DeunaSDK';
3
+ import { DeunaWebView } from './DeunaWebView';
4
+ import { View } from 'react-native';
5
+
6
+ interface DeviceFingerprintWebViewProps {
7
+ instance: DeunaSDK;
8
+ }
9
+
10
+ export const DeviceFingerprintWebView = (
11
+ props: DeviceFingerprintWebViewProps
12
+ ) => {
13
+ const { instance } = props;
14
+ const instanceRef = useRef<DeunaSDK>(instance);
15
+ const [visible, setVisible] = useState(false);
16
+
17
+ useEffect(() => {
18
+ instanceRef.current = instance;
19
+ }, [instance]);
20
+
21
+ // Listen when the DeunaSDK instance configuration
22
+ // has changed
23
+ useEffect(() => {
24
+ const ref = instanceRef.current;
25
+ const listener = () => {
26
+ const isVisible = !!ref.deviceFingerprintController;
27
+ setVisible(isVisible);
28
+ };
29
+
30
+ instanceRef.current.addListener(listener);
31
+
32
+ return () => {
33
+ ref.removeListener(listener);
34
+ };
35
+ }, []);
36
+
37
+ const deviceFingerprintController =
38
+ instanceRef.current.deviceFingerprintController;
39
+
40
+ return (
41
+ <>
42
+ {visible && (
43
+ <View>
44
+ <DeunaWebView
45
+ url={deviceFingerprintController?.url ?? ''}
46
+ onWebView={deviceFingerprintController?.setWebView}
47
+ onMessage={deviceFingerprintController?.onMessage}
48
+ onLoad={deviceFingerprintController?.onLoad}
49
+ onError={deviceFingerprintController?.onError}
50
+ onShouldStartLoadWithRequest={
51
+ deviceFingerprintController?.onShouldStartLoadWithRequest
52
+ }
53
+ />
54
+ </View>
55
+ )}
56
+ </>
57
+ );
58
+ };
@@ -51,6 +51,9 @@ export const NewTabWebView = (props: NewTabWebViewProps) => {
51
51
  onMessage={newTabController?.onMessage}
52
52
  onLoad={newTabController?.onLoad}
53
53
  onError={newTabController?.onError}
54
+ onShouldStartLoadWithRequest={
55
+ newTabController?.onShouldStartLoadWithRequest
56
+ }
54
57
  />
55
58
  </SafeAreaView>
56
59
  )}
@@ -9,11 +9,15 @@ import {
9
9
  SubmitResult,
10
10
  } from '../types';
11
11
  import { submitError } from '../interfaces';
12
+ import { Platform } from 'react-native';
13
+ import { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTypes';
12
14
 
13
15
  export interface WebViewDelegate {
14
16
  onOpenInNewTab?: (url: string) => void;
15
17
  onCloseButtonPressed?: () => void;
16
18
  onCloseSubWebView?: () => void;
19
+ onFileDownload?: (url: string) => void;
20
+ onNewTabWindowClose?: () => void;
17
21
  }
18
22
 
19
23
  export enum WebViewEventType {
@@ -22,15 +26,26 @@ export enum WebViewEventType {
22
26
  jsExecutor = 'jsExecutor',
23
27
  openInNewTab = 'openInNewTab',
24
28
  redirect = 'redirect',
29
+ newTabWindowClose = 'newTabWindowClose',
25
30
  }
26
31
 
32
+ const DOWNLOAD_FILE_REGEX =
33
+ /\.(pdf|doc|docx|xls|xlsx|zip|rar|txt|mp3|mp4|jpg|jpeg|png|gif)$/i;
34
+
27
35
  export abstract class BaseWebViewController {
36
+ initialized = false;
37
+ redirectUrl: string | null = null;
28
38
  webView: WebView | null = null;
29
39
  url: string | null = null;
30
40
 
31
41
  delegate: WebViewDelegate | null = null;
42
+ jsExecutor = new JsExecutor();
43
+
44
+ setWebView = (webView: WebView) => {
45
+ this.webView = webView;
46
+ this.jsExecutor.webView = webView;
47
+ };
32
48
 
33
- abstract setWebView: (webView: WebView) => void;
34
49
  abstract onMessage: (event: WebViewMessageEvent) => void;
35
50
 
36
51
  /**
@@ -43,6 +58,52 @@ export abstract class BaseWebViewController {
43
58
  */
44
59
  abstract onError: (event: any) => void;
45
60
 
61
+ /**
62
+ * Called when the web view should load a URL in the same web view
63
+ */
64
+ abstract urlMustBeLoadedInTheSameWebView: (url: string) => boolean;
65
+
66
+ /**
67
+ * Checks if the URL is a download URL
68
+ * @param url - The URL to check
69
+ * @returns True if the URL is a download URL, false otherwise
70
+ */
71
+ isDownloadUrl = (url: string) => {
72
+ return DOWNLOAD_FILE_REGEX.test(url);
73
+ };
74
+
75
+ /**
76
+ * Determines if a URL should be loaded in the current WebView or opened in a new tab
77
+ * @param request The navigation request details
78
+ * @returns boolean indicating if the URL should be loaded in current WebView
79
+ */
80
+ onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
81
+ // Prevent loading if it's the redirect URL
82
+ if (this.redirectUrl === request.url) {
83
+ return false;
84
+ }
85
+
86
+ const isNewNavigation = request.isTopFrame && request.url !== this.url;
87
+ const isClickNavigation = request.navigationType === 'click';
88
+ const shouldHandleInNewTab = !this.urlMustBeLoadedInTheSameWebView(
89
+ request.url
90
+ );
91
+
92
+ // For iOS, check both click navigation and new navigation
93
+ // For Android, only check new navigation
94
+ const shouldOpenInNewTab =
95
+ Platform.OS === 'ios'
96
+ ? (isClickNavigation || isNewNavigation) && shouldHandleInNewTab
97
+ : isNewNavigation && shouldHandleInNewTab;
98
+
99
+ if (shouldOpenInNewTab) {
100
+ this.delegate?.onOpenInNewTab?.(request.url);
101
+ return false;
102
+ }
103
+
104
+ return true;
105
+ };
106
+
46
107
  /**
47
108
  * Release the web view resources
48
109
  */
@@ -56,19 +117,22 @@ export abstract class DeunaWebViewController extends BaseWebViewController {
56
117
  hidePayButton = false;
57
118
  redirectUrl: string | null = null;
58
119
  closedAction: ClosedAction = 'systemAction';
59
- jsExecutor = new JsExecutor();
60
120
 
61
121
  abstract onEventDispatch: (event: Record<string, any>) => void;
62
122
 
63
- setWebView = (webView: WebView) => {
64
- this.webView = webView;
65
- this.jsExecutor.webView = webView;
66
- };
67
-
68
123
  onLoad = () => {
69
124
  this.setXprops();
70
125
  };
71
126
 
127
+ urlMustBeLoadedInTheSameWebView = (url: string) => {
128
+ if (this.isDownloadUrl(url)) {
129
+ this.delegate?.onFileDownload?.(url);
130
+ } else {
131
+ this.redirectUrl = url;
132
+ }
133
+ return false;
134
+ };
135
+
72
136
  onMessage = (event: WebViewMessageEvent) => {
73
137
  const eventData = JSON.parse(event.nativeEvent.data);
74
138
 
@@ -100,6 +164,9 @@ export abstract class DeunaWebViewController extends BaseWebViewController {
100
164
  this.redirectUrl = eventData.url;
101
165
  this.delegate?.onOpenInNewTab?.(eventData.url);
102
166
  },
167
+ [WebViewEventType.newTabWindowClose]: () => {
168
+ this.delegate?.onNewTabWindowClose?.();
169
+ },
103
170
  };
104
171
 
105
172
  mapper[eventData.type as WebViewEventType]?.();
@@ -108,19 +175,9 @@ export abstract class DeunaWebViewController extends BaseWebViewController {
108
175
  setXprops = () => {
109
176
  this.webView?.injectJavaScript(
110
177
  `
111
- document.addEventListener('click', function(event) {
112
- if (event.target.tagName === 'A' && event.target.href) {
113
- window.ReactNativeWebView.postMessage(JSON.stringify({ type: '${WebViewEventType.redirect}', url: event.target.href }));
114
- event.preventDefault(); // Prevent default navigation
115
- event.stopPropagation();
116
- }
117
- });
118
178
  console.log = function(message) {
119
179
  window.ReactNativeWebView.postMessage(JSON.stringify({ type: '${WebViewEventType.consoleLog}', message }));
120
180
  };
121
- window.open = function(url, target, features) {
122
- window.ReactNativeWebView.postMessage(JSON.stringify({ type: '${WebViewEventType.openInNewTab}', url }));
123
- };
124
181
  window.xprops = {
125
182
  hidePayButton: ${this.hidePayButton},
126
183
  onEventDispatch: function (event) {
@@ -166,8 +223,8 @@ export abstract class DeunaWebViewController extends BaseWebViewController {
166
223
  * @returns The order data | returns null if the order is not found or an error occurs
167
224
  */
168
225
  refetchOrder = async (): Promise<Json | null> => {
169
- const result = await this.jsExecutor.execute(`
170
- if(typeof window.deunaRefetchOrder !== 'function'){
226
+ const result = await this.jsExecutor
227
+ .execute(` if(typeof window.deunaRefetchOrder !== 'function'){
171
228
  sendResult({order: null});
172
229
  return;
173
230
  }
@@ -0,0 +1,86 @@
1
+ import { WebViewMessageEvent } from 'react-native-webview';
2
+ import {
3
+ BaseWebViewController,
4
+ WebViewEventType,
5
+ } from './BaseWebViewController';
6
+ import { Completer } from '../helpers/Completer';
7
+ import { Environment } from '../types';
8
+ import { DeunaLogs } from '../DeunaLogs';
9
+
10
+ export class DeviceFingerprintController extends BaseWebViewController {
11
+ constructor(
12
+ private readonly publicApiKey: string,
13
+ private readonly environment: Environment
14
+ ) {
15
+ super();
16
+ }
17
+
18
+ private completer = new Completer<boolean>();
19
+
20
+ onMessage = (event: WebViewMessageEvent) => {
21
+ const eventData = JSON.parse(event.nativeEvent.data);
22
+
23
+ const mapper = {
24
+ [WebViewEventType.consoleLog]: () => {
25
+ DeunaLogs.info(
26
+ `CONSOLE LOG`,
27
+ JSON.stringify(eventData.message, null, 2)
28
+ );
29
+ },
30
+ [WebViewEventType.jsExecutor]: () => {
31
+ const { data, requestId } = eventData as {
32
+ requestId: number;
33
+ data: Record<string, any> | null;
34
+ };
35
+ this.jsExecutor.requests.get(requestId)?.(data);
36
+ this.jsExecutor.requests.delete(requestId);
37
+ },
38
+ };
39
+ mapper[eventData.type as keyof typeof mapper]?.();
40
+ };
41
+
42
+ onLoad = () => {
43
+ this.webView?.injectJavaScript(`
44
+ console.log = function(message) {
45
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: '${WebViewEventType.consoleLog}', message:message }));
46
+ };
47
+ `);
48
+ this.completer.complete(true);
49
+ };
50
+
51
+ onError = (_: any) => {
52
+ this.completer.complete(false);
53
+ };
54
+
55
+ urlMustBeLoadedInTheSameWebView = (_: string) => true;
56
+
57
+ generateDeviceFingerprint = async (
58
+ params: Record<string, any>
59
+ ): Promise<string | null> => {
60
+ const isLoaded = await this.completer.wait;
61
+ if (!isLoaded) {
62
+ return null;
63
+ }
64
+
65
+ const data = await this.jsExecutor.execute(`
66
+ if (typeof window.generateFraudId !== 'function') {
67
+ console.log('❌ window.generateFraudId is not a function');
68
+ sendResult({fraudId: null});
69
+ return;
70
+ }
71
+
72
+ window.generateFraudId({
73
+ publicApiKey: "${this.publicApiKey}",
74
+ env: "${this.environment}",
75
+ params: ${JSON.stringify(params)}
76
+ })
77
+ .then((fraudId) => {
78
+ sendResult({ fraudId: fraudId });
79
+ })
80
+ .catch((error) => {
81
+ sendResult({ fraudId: null });
82
+ });
83
+ `);
84
+ return data?.fraudId ?? null;
85
+ };
86
+ }
@@ -1,5 +1,8 @@
1
- import WebView, { WebViewMessageEvent } from 'react-native-webview';
2
- import { BaseWebViewController } from './BaseWebViewController';
1
+ import { WebViewMessageEvent } from 'react-native-webview';
2
+ import {
3
+ BaseWebViewController,
4
+ WebViewEventType,
5
+ } from './BaseWebViewController';
3
6
  import { DeunaLogs } from '../DeunaLogs';
4
7
 
5
8
  export class OpenInNewTabController extends BaseWebViewController {
@@ -8,10 +11,6 @@ export class OpenInNewTabController extends BaseWebViewController {
8
11
  this.url = url;
9
12
  }
10
13
 
11
- setWebView = (webView: WebView) => {
12
- this.webView = webView;
13
- };
14
-
15
14
  onLoad = () => {
16
15
  this.setXprops();
17
16
  };
@@ -20,6 +19,14 @@ export class OpenInNewTabController extends BaseWebViewController {
20
19
  console.warn(event);
21
20
  };
22
21
 
22
+ urlMustBeLoadedInTheSameWebView = (url: string) => {
23
+ if (this.isDownloadUrl(url)) {
24
+ this.delegate?.onFileDownload?.(url);
25
+ return false;
26
+ }
27
+ return true;
28
+ };
29
+
23
30
  onMessage = (event: WebViewMessageEvent) => {
24
31
  const eventData = JSON.parse(event.nativeEvent.data);
25
32
  if (eventData.type === 'console_log') {
@@ -28,12 +35,22 @@ export class OpenInNewTabController extends BaseWebViewController {
28
35
  };
29
36
 
30
37
  setXprops = () => {
31
- this.webView?.injectJavaScript(
32
- `
33
- console.log = function(message) {
34
- window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'console_log', message }));
35
- };
36
- `
37
- );
38
+ // Hide the print button on the Efecty page
39
+ this.webView?.injectJavaScript(`
40
+ console.log = function(message) {
41
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: '${WebViewEventType.consoleLog}', message }));
42
+ };
43
+ window.close = function() {
44
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: '${WebViewEventType.newTabWindowClose}', data: '' }));
45
+ };
46
+ (function() {
47
+ setTimeout(function() {
48
+ var button = document.getElementById("cash_efecty_button_print");
49
+ if (button) {
50
+ button.style.display = "none";
51
+ }
52
+ }, 500); // time out 500 ms
53
+ })();
54
+ `);
38
55
  };
39
56
  }
@@ -3,3 +3,6 @@ export const constants = {
3
3
  metadata: 'metadata',
4
4
  voucherPdfDownloadUrl: 'voucherPdfDownloadUrl',
5
5
  };
6
+
7
+
8
+ export const DEVICE_FINGERPRINT_URL = 'https://cdn.stg.deuna.io/mobile-sdks/get_fraud_id.html';
@@ -2,86 +2,119 @@ import { Environment } from "../base";
2
2
  import { hasKey } from "../utils/hasKey";
3
3
  import { getIntegrationType, UrlConfig } from "./urlConfig";
4
4
 
5
- const urls: Record<Environment, string> = {
5
+ const ELEMENTS_URLS: Record<Environment, string> = {
6
6
  production: "https://elements.deuna.io",
7
7
  sandbox: "https://elements.sandbox.deuna.io",
8
8
  staging: "https://elements.stg.deuna.io",
9
9
  develop: "https://elements.dev.deuna.io",
10
- };
10
+ } as const;
11
11
 
12
- const pathsMapper = {
12
+ const WIDGET_PATHS = {
13
13
  click_to_pay: "/click_to_pay",
14
14
  vault: "/vault",
15
+ } as const;
16
+
17
+ type WidgetPath = keyof typeof WIDGET_PATHS;
18
+
19
+ interface SearchParams {
20
+ publicApiKey: string;
21
+ orderToken: string;
22
+ email: string;
23
+ firstName: string;
24
+ lastName: string;
25
+ int: string;
26
+ language: string;
27
+ cssFile?: string;
28
+ userToken?: string;
29
+ showSavedCardsFlow?: string;
30
+ showDefaultCardFlow?: string;
31
+ }
32
+
33
+ /**
34
+ * Builds the base URL for the Elements widget based on environment and domain configuration
35
+ */
36
+ const buildBaseUrl = (config: UrlConfig): string => {
37
+ if (config.domain) {
38
+ return config.domain;
39
+ }
40
+ return config.env ? ELEMENTS_URLS[config.env] : ELEMENTS_URLS.production;
15
41
  };
16
42
 
17
- export const buildElementsLink = (config: UrlConfig): string => {
18
- const { env, orderToken, userInfo, userToken, widgetExperience, types } =
43
+ /**
44
+ * Builds the search parameters for the Elements widget URL
45
+ */
46
+ const buildSearchParams = (config: UrlConfig): URLSearchParams => {
47
+ const { userInfo, userToken, widgetExperience, styleFile, mode, language } =
19
48
  config;
20
49
 
21
- let baseUrl = urls.production;
50
+ const searchParams: SearchParams = {
51
+ publicApiKey: config.publicApiKey,
52
+ orderToken: config.orderToken,
53
+ email: userInfo?.email || "",
54
+ firstName: userInfo?.firstName || "",
55
+ lastName: userInfo?.lastName || "",
56
+ int: getIntegrationType(mode),
57
+ language,
58
+ };
22
59
 
23
- if (env) {
24
- baseUrl = urls[env];
60
+ if (styleFile) {
61
+ searchParams.cssFile = styleFile;
25
62
  }
26
63
 
27
- if (config.domain) {
28
- baseUrl = config.domain;
64
+ if (userToken) {
65
+ searchParams.userToken = userToken;
29
66
  }
30
67
 
31
- const url = new URL(baseUrl);
32
-
33
- const publicApiKey = config.publicApiKey;
34
- const firstName = userInfo?.firstName || "";
35
- const lastName = userInfo?.lastName || "";
36
- const email = userInfo?.email || "";
37
-
38
- const searchParams = new URLSearchParams({
39
- publicApiKey,
40
- orderToken,
41
- email,
42
- firstName,
43
- lastName,
44
- ...(config.styleFile ? { cssFile: config.styleFile } : {}),
45
- int: getIntegrationType(config.mode),
46
- ...(userToken ? { userToken } : {}),
47
- ...(widgetExperience?.userExperience?.showSavedCardsFlow
48
- ? {
49
- showSavedCardsFlow: String(
50
- widgetExperience?.userExperience?.showSavedCardsFlow
51
- ),
52
- }
53
- : {}),
54
- ...(widgetExperience?.userExperience?.defaultCardFlow
55
- ? {
56
- showDefaultCardFlow: String(
57
- widgetExperience?.userExperience?.defaultCardFlow
58
- ),
59
- }
60
- : {}),
61
- language: config.language,
62
- });
68
+ if (widgetExperience?.userExperience?.showSavedCardsFlow) {
69
+ searchParams.showSavedCardsFlow = String(
70
+ widgetExperience.userExperience.showSavedCardsFlow
71
+ );
72
+ }
63
73
 
64
- const xpropsB64 = {
65
- id: config.id,
66
- behavior: config.behavior || {},
67
- };
74
+ if (widgetExperience?.userExperience?.defaultCardFlow) {
75
+ searchParams.showDefaultCardFlow = String(
76
+ widgetExperience.userExperience.defaultCardFlow
77
+ );
78
+ }
79
+
80
+ return new URLSearchParams(searchParams as unknown as Record<string, string>);
81
+ };
68
82
 
69
- const getWidgetPath = () => {
70
- if (types && types.length > 0) {
71
- const firstPath = types[0]!.name;
83
+ /**
84
+ * Determines the widget path based on the provided types
85
+ */
86
+ const getWidgetPath = (types?: Array<{ name: string }>): string => {
87
+ if (types && types.length > 0) {
88
+ const firstPath = types[0]?.name as WidgetPath;
89
+ return hasKey(WIDGET_PATHS, firstPath)
90
+ ? WIDGET_PATHS[firstPath]
91
+ : WIDGET_PATHS.vault;
92
+ }
93
+ return WIDGET_PATHS.vault;
94
+ };
72
95
 
73
- if (hasKey(pathsMapper, firstPath)) {
74
- return pathsMapper[firstPath];
75
- }
76
- }
96
+ /**
97
+ * Builds a complete Elements widget URL with all necessary parameters
98
+ * @param config - Configuration object for building the Elements URL
99
+ * @returns The complete Elements widget URL as a string
100
+ */
101
+ export const buildElementsLink = (config: UrlConfig): string => {
102
+ const baseUrl = buildBaseUrl(config);
103
+ const searchParams = buildSearchParams(config);
77
104
 
78
- return pathsMapper.vault;
105
+ const xpropsB64 = {
106
+ id: config.id,
107
+ behavior: config.behavior || {},
79
108
  };
80
109
 
81
110
  searchParams.append("xpropsB64", btoa(JSON.stringify(xpropsB64)));
82
111
 
83
- url.pathname = getWidgetPath();
84
- url.search = searchParams.toString();
112
+ const widgetPath = getWidgetPath(config.types);
113
+ const normalizedPath = widgetPath.startsWith("/")
114
+ ? widgetPath
115
+ : `/${widgetPath}`;
116
+
117
+ const url = new URL(`${baseUrl}${normalizedPath}?${searchParams.toString()}`);
85
118
 
86
119
  return url.toString();
87
120
  };
@@ -21,22 +21,26 @@ export const buildNextActionLink = (config: UrlConfig): string => {
21
21
  baseUrl = urls[env];
22
22
  }
23
23
 
24
+ const xpropsB64 = {
25
+ id: config.id
26
+ };
27
+
24
28
  const searchParams = new URLSearchParams({
25
29
  mode: "widget",
26
30
  int: getIntegrationType(config.mode),
27
31
  language: config.language,
28
32
  });
29
33
 
34
+ //encode to base64
35
+ searchParams.append("xpropsB64", btoa(JSON.stringify(xpropsB64)));
36
+
30
37
  configToQueryParams(config, searchParams);
31
38
 
32
39
  if (config.domain) {
33
40
  baseUrl = config.domain;
34
41
  }
35
42
 
36
- const url = new URL(baseUrl);
37
-
38
- url.pathname = `/next-action-purchase/${orderToken}`;
39
- url.search = searchParams.toString();
43
+ const url = new URL(`${baseUrl}/next-action-purchase/${orderToken}?${searchParams.toString()}`);
40
44
 
41
45
  return url.toString();
42
46
  };
@@ -54,10 +54,7 @@ export const buildPaymentLink = (config: UrlConfig): string => {
54
54
  //encode to base64
55
55
  searchParams.append("xpropsB64", btoa(JSON.stringify(xpropsB64)));
56
56
 
57
- const url = new URL(baseUrl);
58
-
59
- url.pathname = `/now/${orderToken}`;
60
- url.search = searchParams.toString();
57
+ const url = new URL(`${baseUrl}/now/${orderToken}?${searchParams.toString()}`);
61
58
 
62
59
  return url.toString();
63
60
  };
@@ -44,10 +44,7 @@ export const buildVoucherLink = (config: UrlConfig): string => {
44
44
  //encode to base64
45
45
  searchParams.append("xpropsB64", btoa(JSON.stringify(xpropsB64)));
46
46
 
47
- const url = new URL(baseUrl);
48
-
49
- url.pathname = `/voucher`;
50
- url.search = searchParams.toString();
47
+ const url = new URL(`${baseUrl}/voucher?${searchParams.toString()}`);
51
48
 
52
49
  return url.toString();
53
50
  };