@tryfinch/react-connect 3.15.1 → 4.1.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/src/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
 
3
- type HasKey<T, K extends PropertyKey> = T extends Record<K, unknown> ? T : never;
4
-
5
3
  export type SuccessEvent = {
6
4
  code: string;
7
5
  state?: string;
@@ -9,50 +7,36 @@ export type SuccessEvent = {
9
7
  };
10
8
 
11
9
  type ErrorType = 'validation_error' | 'employer_connection_error';
12
-
13
10
  export type ErrorEvent = {
14
11
  errorMessage: string;
15
12
  errorType?: ErrorType;
16
13
  };
17
14
 
18
- export type Sandbox =
19
- | 'finch' /** This is to enable the new Finch (simulated) Sandbox */
20
- | 'provider' /** This is to enable the new Provider Sandbox */
21
- | boolean /** This is the old sandbox flag retained for backwards compatibility */;
15
+ type ApiConfig = {
16
+ connectUrl: string;
17
+ };
22
18
 
23
- type BaseConnectOptions = {
24
- state: string | null;
19
+ export type ConnectInitializeArgs = {
25
20
  onSuccess: (e: SuccessEvent) => void;
26
21
  onError: (e: ErrorEvent) => void;
27
22
  onClose: () => void;
28
- zIndex: number;
29
- apiConfig?: {
30
- connectUrl: string;
31
- redirectUrl: string;
32
- };
23
+ apiConfig?: ApiConfig;
33
24
  };
34
25
 
35
- type ConnectOptionsWithSessionId = BaseConnectOptions & {
36
- // Use this option if you have a Finch Connect sessionID from the IDP redirect flow
26
+ export type ConnectLaunchArgs = {
37
27
  sessionId: string;
38
- // Allow for overriding products for the session
39
- products?: string[];
28
+ state?: string;
29
+ zIndex?: number;
40
30
  };
41
31
 
42
- type ConnectOptionsWithClientId = BaseConnectOptions & {
43
- category: string | null;
32
+ export type ConnectPreviewLaunchArgs = {
44
33
  clientId: string;
45
- manual: boolean;
46
- payrollProvider: string | null;
47
34
  products: string[];
48
- clientName?: string;
49
- connectionId?: string;
50
- sandbox: Sandbox;
51
35
  };
52
36
 
53
- export type ConnectOptions = ConnectOptionsWithSessionId | ConnectOptionsWithClientId;
37
+ type OpenFn = (args: ConnectLaunchArgs) => void;
54
38
 
55
- type OpenFn = (overrides?: Partial<ConnectOptions>) => void;
39
+ type OpenPreviewFn = (args: ConnectPreviewLaunchArgs) => void;
56
40
 
57
41
  const POST_MESSAGE_NAME = 'finch-auth-message-v2' as const;
58
42
 
@@ -78,95 +62,69 @@ interface FinchConnectPostMessage {
78
62
  }
79
63
 
80
64
  const BASE_FINCH_CONNECT_URI = 'https://connect.tryfinch.com';
81
- const DEFAULT_FINCH_REDIRECT_URI = 'https://tryfinch.com';
82
65
 
83
66
  const FINCH_CONNECT_IFRAME_ID = 'finch-connect-iframe';
84
67
 
85
- const constructAuthUrl = (connectOptions: ConnectOptions) => {
86
- const { state, apiConfig } = connectOptions;
68
+ const appendBaseParams = (url: URL) => {
69
+ url.searchParams.append('app_type', 'spa');
70
+ // The host URL of the SDK. This is used to store the referrer for postMessage purposes
71
+ url.searchParams.append('sdk_host_url', window.location.origin);
72
+ url.searchParams.append('mode', 'employer');
73
+ // replace with actual SDK version by rollup
74
+ url.searchParams.append('sdk_version', 'react-SDK_VERSION');
75
+ };
87
76
 
77
+ export const constructAuthUrl = ({
78
+ sessionId,
79
+ state,
80
+ apiConfig,
81
+ }: {
82
+ sessionId: string;
83
+ state?: string;
84
+ apiConfig?: ApiConfig;
85
+ }) => {
88
86
  const CONNECT_URL = apiConfig?.connectUrl || BASE_FINCH_CONNECT_URI;
89
- const REDIRECT_URL = apiConfig?.redirectUrl || DEFAULT_FINCH_REDIRECT_URI;
90
87
 
91
88
  const authUrl = new URL(`${CONNECT_URL}/authorize`);
92
89
 
93
- if ('sessionId' in connectOptions) {
94
- const { sessionId, products } = connectOptions;
95
- authUrl.searchParams.append('session', sessionId);
96
- if (products) authUrl.searchParams.append('products', products.join(' '));
97
- } else {
98
- const {
99
- clientId,
100
- payrollProvider,
101
- category,
102
- products,
103
- manual,
104
- sandbox,
105
- clientName,
106
- connectionId,
107
- } = connectOptions;
108
-
109
- if (clientId) authUrl.searchParams.append('client_id', clientId);
110
- if (payrollProvider) authUrl.searchParams.append('payroll_provider', payrollProvider);
111
- if (category) authUrl.searchParams.append('category', category);
112
- if (clientName) authUrl.searchParams.append('client_name', clientName);
113
- if (connectionId) authUrl.searchParams.append('connection_id', connectionId);
114
- authUrl.searchParams.append('products', (products ?? []).join(' '));
115
- if (manual) authUrl.searchParams.append('manual', String(manual));
116
- if (sandbox) authUrl.searchParams.append('sandbox', String(sandbox));
117
- }
118
-
119
- authUrl.searchParams.append('app_type', 'spa');
120
- authUrl.searchParams.append('redirect_uri', REDIRECT_URL);
121
- /** The host URL of the SDK. This is used to store the referrer for postMessage purposes */
122
- authUrl.searchParams.append('sdk_host_url', window.location.origin);
123
- authUrl.searchParams.append('mode', 'employer');
90
+ authUrl.searchParams.append('session', sessionId);
124
91
  if (state) authUrl.searchParams.append('state', state);
125
- // replace with actual SDK version by rollup
126
- authUrl.searchParams.append('sdk_version', 'react-SDK_VERSION');
92
+
93
+ appendBaseParams(authUrl);
127
94
 
128
95
  return authUrl.href;
129
96
  };
130
97
 
131
- const noop = () => {
132
- // intentionally empty
133
- };
98
+ export const constructPreviewUrl = ({
99
+ clientId,
100
+ products,
101
+ apiConfig,
102
+ }: {
103
+ clientId: string;
104
+ products: string[];
105
+ apiConfig?: ApiConfig;
106
+ }) => {
107
+ const CONNECT_URL = apiConfig?.connectUrl || BASE_FINCH_CONNECT_URI;
134
108
 
135
- const BASE_DEFAULTS = {
136
- onSuccess: noop,
137
- onError: noop,
138
- onClose: noop,
139
- state: null,
140
- zIndex: 999,
141
- };
109
+ const previewUrl = new URL(`${CONNECT_URL}/authorize`);
142
110
 
143
- const DEFAULT_OPTIONS_WITH_CLIENT_ID: HasKey<ConnectOptions, 'clientId'> = {
144
- ...BASE_DEFAULTS,
145
- clientId: '',
146
- category: null,
147
- manual: false,
148
- payrollProvider: null,
149
- products: [],
150
- clientName: undefined,
151
- sandbox: false,
152
- };
111
+ previewUrl.searchParams.append('preview', 'true');
112
+ previewUrl.searchParams.append('client_id', clientId);
113
+ previewUrl.searchParams.append('products', (products ?? []).join(' '));
114
+ // This will be replaced by a universally allowed redirect URI in the backend
115
+ // We won't ever redirect to anything in preview mode, so we just need a value to pass validation
116
+ previewUrl.searchParams.append('redirect_uri', 'https://www.tryfinch.com');
117
+
118
+ appendBaseParams(previewUrl);
153
119
 
154
- const DEFAULT_OPTIONS_WITH_SESSION_ID: HasKey<ConnectOptions, 'sessionId'> = {
155
- ...BASE_DEFAULTS,
156
- sessionId: '',
120
+ return previewUrl.href;
157
121
  };
158
122
 
159
123
  let isUseFinchConnectInitialized = false;
160
124
 
161
- export const useFinchConnect = (options: Partial<ConnectOptions>): { open: OpenFn } => {
162
- if (!('sessionId' in options) && !('clientId' in options)) {
163
- throw new Error('must specify either sessionId or clientId in options for useFinchConnect');
164
- }
165
-
166
- if ('sessionId' in options && 'clientId' in options) {
167
- throw new Error('cannot specify both sessionId and clientId in options for useFinchConnect');
168
- }
169
-
125
+ export const useFinchConnect = (
126
+ initializeArgs: ConnectInitializeArgs
127
+ ): { open: OpenFn; openPreview: OpenPreviewFn } => {
170
128
  const isHookMounted = useRef(false);
171
129
 
172
130
  useEffect(() => {
@@ -183,24 +141,20 @@ export const useFinchConnect = (options: Partial<ConnectOptions>): { open: OpenF
183
141
  }
184
142
  }, []);
185
143
 
186
- const combinedOptions: ConnectOptions =
187
- 'sessionId' in options
188
- ? { ...DEFAULT_OPTIONS_WITH_SESSION_ID, ...options }
189
- : { ...DEFAULT_OPTIONS_WITH_CLIENT_ID, ...options };
190
-
191
- const open: OpenFn = (overrides) => {
192
- const openOptions: ConnectOptions = {
193
- ...combinedOptions,
194
- ...overrides,
195
- };
196
-
144
+ const createAndAttachIFrame = ({
145
+ src,
146
+ zIndexOverride,
147
+ }: {
148
+ src: string;
149
+ zIndexOverride?: number;
150
+ }) => {
197
151
  if (!document.getElementById(FINCH_CONNECT_IFRAME_ID)) {
198
152
  const iframe = document.createElement('iframe');
199
- iframe.src = constructAuthUrl(openOptions);
153
+ iframe.src = src;
200
154
  iframe.frameBorder = '0';
201
155
  iframe.id = FINCH_CONNECT_IFRAME_ID;
202
156
  iframe.style.position = 'fixed';
203
- iframe.style.zIndex = openOptions.zIndex.toString();
157
+ iframe.style.zIndex = zIndexOverride?.toString() || '999';
204
158
  iframe.style.height = '100%';
205
159
  iframe.style.width = '100%';
206
160
  iframe.style.top = '0';
@@ -212,6 +166,31 @@ export const useFinchConnect = (options: Partial<ConnectOptions>): { open: OpenF
212
166
  }
213
167
  };
214
168
 
169
+ const open: OpenFn = (launchArgs) => {
170
+ const iframeSrc = constructAuthUrl({
171
+ sessionId: launchArgs.sessionId,
172
+ state: launchArgs.state,
173
+ apiConfig: initializeArgs.apiConfig,
174
+ });
175
+
176
+ createAndAttachIFrame({
177
+ src: iframeSrc,
178
+ zIndexOverride: launchArgs.zIndex,
179
+ });
180
+ };
181
+
182
+ const openPreview: OpenPreviewFn = (launchArgs) => {
183
+ const iframeSrc = constructPreviewUrl({
184
+ clientId: launchArgs.clientId,
185
+ products: launchArgs.products,
186
+ apiConfig: initializeArgs.apiConfig,
187
+ });
188
+
189
+ createAndAttachIFrame({
190
+ src: iframeSrc,
191
+ });
192
+ };
193
+
215
194
  const close = () => {
216
195
  const frameToRemove = document.getElementById(FINCH_CONNECT_IFRAME_ID);
217
196
  if (frameToRemove) {
@@ -222,7 +201,7 @@ export const useFinchConnect = (options: Partial<ConnectOptions>): { open: OpenF
222
201
 
223
202
  useEffect(() => {
224
203
  function handleFinchAuth(event: FinchConnectPostMessage) {
225
- const CONNECT_URL = combinedOptions.apiConfig?.connectUrl || BASE_FINCH_CONNECT_URI;
204
+ const CONNECT_URL = initializeArgs.apiConfig?.connectUrl || BASE_FINCH_CONNECT_URI;
226
205
 
227
206
  if (!event.data) return;
228
207
  if (event.data.name !== POST_MESSAGE_NAME) return;
@@ -232,18 +211,18 @@ export const useFinchConnect = (options: Partial<ConnectOptions>): { open: OpenF
232
211
 
233
212
  switch (event.data.kind) {
234
213
  case 'closed':
235
- combinedOptions.onClose();
214
+ initializeArgs.onClose();
236
215
  break;
237
216
  case 'error':
238
217
  if (event.data.error?.shouldClose) close();
239
218
 
240
- combinedOptions.onError({
219
+ initializeArgs.onError({
241
220
  errorMessage: event.data.error?.message,
242
221
  errorType: event.data.error?.type,
243
222
  });
244
223
  break;
245
224
  case 'success':
246
- combinedOptions.onSuccess({
225
+ initializeArgs.onSuccess({
247
226
  code: event.data.code,
248
227
  state: event.data.state,
249
228
  idpRedirectUri: event.data.idpRedirectUri,
@@ -251,7 +230,7 @@ export const useFinchConnect = (options: Partial<ConnectOptions>): { open: OpenF
251
230
  break;
252
231
  default: {
253
232
  // This case should never happen, if it does it should be reported to us
254
- combinedOptions.onError({
233
+ initializeArgs.onError({
255
234
  errorMessage: `Report to developers@tryfinch.com: unable to handle window.postMessage for: ${JSON.stringify(
256
235
  event.data
257
236
  )}`,
@@ -265,9 +244,10 @@ export const useFinchConnect = (options: Partial<ConnectOptions>): { open: OpenF
265
244
  window.removeEventListener('message', handleFinchAuth);
266
245
  isUseFinchConnectInitialized = false;
267
246
  };
268
- }, [combinedOptions.onClose, combinedOptions.onError, combinedOptions.onSuccess]);
247
+ }, [initializeArgs.onClose, initializeArgs.onError, initializeArgs.onSuccess]);
269
248
 
270
249
  return {
271
250
  open,
251
+ openPreview,
272
252
  };
273
253
  };