@parafin/core 2.3.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/index.ts CHANGED
@@ -34,23 +34,31 @@ export const openParafinDashboard = (
34
34
  )
35
35
  const route = 'route' in props ? props.route : '/'
36
36
 
37
- const addPropIfExists = (key: string) => {
38
- if (key in props && props[key] !== undefined && props[key] !== null) {
39
- return { [key]: props[key] }
40
- } else {
41
- return {}
42
- }
43
- }
44
-
45
37
  const query = {
46
38
  product: props.product,
47
39
  referrer: 'partner',
48
- ...addPropIfExists('token'),
49
- ...addPropIfExists('partner'),
50
- ...addPropIfExists('externalBusinessId'),
51
- ...addPropIfExists('orderId'),
52
- ...addPropIfExists('lineOfCreditApplicationId'),
53
- ...addPropIfExists('inWebView'),
40
+ ...('token' in props &&
41
+ props.token !== undefined &&
42
+ props.token !== null && { token: props.token }),
43
+ ...('partner' in props &&
44
+ props.partner !== undefined &&
45
+ props.partner !== null && { partner: props.partner }),
46
+ ...('externalBusinessId' in props &&
47
+ props.externalBusinessId !== undefined &&
48
+ props.externalBusinessId !== null && {
49
+ externalBusinessId: props.externalBusinessId,
50
+ }),
51
+ ...('orderId' in props &&
52
+ props.orderId !== undefined &&
53
+ props.orderId !== null && { orderId: props.orderId }),
54
+ ...('lineOfCreditApplicationId' in props &&
55
+ props.lineOfCreditApplicationId !== undefined &&
56
+ props.lineOfCreditApplicationId !== null && {
57
+ lineOfCreditApplicationId: props.lineOfCreditApplicationId,
58
+ }),
59
+ ...('inWebView' in props &&
60
+ props.inWebView !== undefined &&
61
+ props.inWebView !== null && { inWebView: props.inWebView.toString() }),
54
62
  ...Object.fromEntries(searchParams),
55
63
  }
56
64
 
@@ -58,12 +66,12 @@ export const openParafinDashboard = (
58
66
 
59
67
  if ('openInNewTab' in props && props.openInNewTab) {
60
68
  window.open(url, '_blank')
61
- return
69
+ return () => {} // noop
62
70
  }
63
71
 
64
72
  if ('mode' in props && props.mode === 'redirect') {
65
73
  window.location.href = url
66
- return
74
+ return () => {} // noop
67
75
  }
68
76
 
69
77
  const existingParafinDashboard =
@@ -107,17 +115,26 @@ export const openParafinDashboard = (
107
115
  }
108
116
  }
109
117
 
118
+ let cleanedUp = false
119
+ const cleanup = ({ withOnExit }: { withOnExit: boolean }) => {
120
+ if (cleanedUp) return // noop
121
+ cleanedUp = true
122
+
123
+ window.removeEventListener('message', messageListener)
124
+ frame.style.opacity = '0'
125
+
126
+ setTimeout(() => {
127
+ if (withOnExit) onExit()
128
+ dashboardTargetElement.removeChild(frame)
129
+ document.body.style.removeProperty('overflow')
130
+ }, 200)
131
+ }
132
+
110
133
  const messageListener = async (event: MessageEvent) => {
111
134
  if (event.origin === origin) {
112
135
  switch (event.data?.message) {
113
136
  case 'close-dashboard':
114
- window.removeEventListener('message', messageListener)
115
- frame.style.opacity = '0'
116
- setTimeout(() => {
117
- onExit()
118
- dashboardTargetElement.removeChild(frame)
119
- document.body.style.removeProperty('overflow')
120
- }, 200)
137
+ cleanup({ withOnExit: true })
121
138
  break
122
139
  case 'link-opened':
123
140
  if (props.onLinkOpened && event.data?.url && event.data?.metadata) {
@@ -133,8 +150,115 @@ export const openParafinDashboard = (
133
150
  setTimeout(() => {
134
151
  frame.style.opacity = '1'
135
152
  document.body.style.overflow = 'hidden'
136
- }, 1)
153
+ }, 0)
154
+
155
+ return () => cleanup({ withOnExit: false })
137
156
  } catch (error) {
138
157
  console.error('Error loading Parafin dashboard', error)
158
+ return () => {} // noop
159
+ }
160
+ }
161
+
162
+ export const defaultWidgetStyles = {
163
+ width: '100%',
164
+ height: '258px',
165
+ backgroundColor: '#fff',
166
+ border: '1px solid #E8E8E8',
167
+ borderRadius: '16px',
168
+ transition: 'border 0.2s, border-radius 0.2s',
169
+ boxSizing: 'border-box' as const,
170
+ }
171
+
172
+ export type WidgetEvent = 'opted_in' | 'opted_out'
173
+
174
+ export type WidgetProps = {
175
+ token: string
176
+ product: 'capital' | 'spend_card' | 'cash_account'
177
+ externalBusinessId?: string
178
+ onEvent?: (eventType: WidgetEvent) => Promise<void> | void
179
+ onExit?: () => void
180
+ openInNewTab?: boolean
181
+ onLinkOpened?: (url: string, metadata: LinkOpenedMetadata) => void
182
+ inWebView?: boolean
183
+ }
184
+
185
+ export const initializeParafinWidget = (
186
+ iframe: HTMLIFrameElement,
187
+ props: WidgetProps
188
+ ) => {
189
+ // @ts-ignore
190
+ const url = new URL(props.widgetUrlOverride ?? 'https://widget.parafin.com')
191
+ const query = {
192
+ token: props.token,
193
+ product: props.product,
194
+ host: window.location.origin,
195
+ externalBusinessId: props.externalBusinessId ?? '',
196
+ ...Object.fromEntries(url.searchParams),
197
+ }
198
+ const iframeSrc = `${url.origin}?${new URLSearchParams(query).toString()}`
199
+
200
+ iframe.id = `parafin-${props.product}-widget`
201
+ iframe.src = iframeSrc
202
+
203
+ const sendMessage = (message: any) => {
204
+ iframe.contentWindow?.postMessage(message, url.origin)
139
205
  }
206
+
207
+ const messageListener = async ({ data, origin }: MessageEvent) => {
208
+ if (origin === url.origin && data?.product === props.product) {
209
+ switch (data?.message) {
210
+ case 'set-border':
211
+ if (data?.borderColor) {
212
+ iframe.style.border = `1px solid ${data.borderColor}`
213
+ }
214
+ if (data?.borderRadius) {
215
+ iframe.style.borderRadius = data.borderRadius
216
+ }
217
+ break
218
+ case 'open-dashboard':
219
+ openParafinDashboard({
220
+ ...props,
221
+ route: data?.route,
222
+ onExit: () => {
223
+ iframe.src = iframeSrc
224
+ props.onExit?.()
225
+ },
226
+ })
227
+ break
228
+ case 'person-opt-in':
229
+ if (props.onEvent) {
230
+ try {
231
+ await props.onEvent('opted_in')
232
+ sendMessage({ message: 'person-opt-in', state: 'success' })
233
+ } catch {
234
+ sendMessage({ message: 'person-opt-in', state: 'error' })
235
+ }
236
+ } else {
237
+ sendMessage({ message: 'person-opt-in', state: 'noop' })
238
+ }
239
+ break
240
+ case 'person-opt-out':
241
+ if (props.onEvent) {
242
+ try {
243
+ await props.onEvent('opted_out')
244
+ sendMessage({ message: 'person-opt-out', state: 'success' })
245
+ } catch {
246
+ sendMessage({ message: 'person-opt-out', state: 'error' })
247
+ }
248
+ } else {
249
+ sendMessage({ message: 'person-opt-out', state: 'noop' })
250
+ }
251
+ break
252
+ case 'set-height':
253
+ if (data?.height) {
254
+ iframe.style.height = data.height
255
+ }
256
+ break
257
+ }
258
+ }
259
+ }
260
+
261
+ window.addEventListener('message', messageListener)
262
+
263
+ return () => window.removeEventListener('message', messageListener)
140
264
  }
package/out/index.d.ts CHANGED
@@ -24,5 +24,26 @@ type BNPLProps = {
24
24
  lineOfCreditApplicationId?: string;
25
25
  onExit?: (id?: string) => Promise<void> | void;
26
26
  };
27
- export declare const openParafinDashboard: (props: BaseProps & (CapitalOrSpendProps | BNPLProps)) => void;
27
+ export declare const openParafinDashboard: (props: BaseProps & (CapitalOrSpendProps | BNPLProps)) => () => void;
28
+ export declare const defaultWidgetStyles: {
29
+ width: string;
30
+ height: string;
31
+ backgroundColor: string;
32
+ border: string;
33
+ borderRadius: string;
34
+ transition: string;
35
+ boxSizing: "border-box";
36
+ };
37
+ export type WidgetEvent = 'opted_in' | 'opted_out';
38
+ export type WidgetProps = {
39
+ token: string;
40
+ product: 'capital' | 'spend_card' | 'cash_account';
41
+ externalBusinessId?: string;
42
+ onEvent?: (eventType: WidgetEvent) => Promise<void> | void;
43
+ onExit?: () => void;
44
+ openInNewTab?: boolean;
45
+ onLinkOpened?: (url: string, metadata: LinkOpenedMetadata) => void;
46
+ inWebView?: boolean;
47
+ };
48
+ export declare const initializeParafinWidget: (iframe: HTMLIFrameElement, props: WidgetProps) => () => void;
28
49
  export {};
package/out/index.js CHANGED
@@ -4,33 +4,41 @@ export const openParafinDashboard = (props) => {
4
4
  // @ts-ignore
5
5
  props.dashboardUrlOverride ?? 'https://app.parafin.com');
6
6
  const route = 'route' in props ? props.route : '/';
7
- const addPropIfExists = (key) => {
8
- if (key in props && props[key] !== undefined && props[key] !== null) {
9
- return { [key]: props[key] };
10
- }
11
- else {
12
- return {};
13
- }
14
- };
15
7
  const query = {
16
8
  product: props.product,
17
9
  referrer: 'partner',
18
- ...addPropIfExists('token'),
19
- ...addPropIfExists('partner'),
20
- ...addPropIfExists('externalBusinessId'),
21
- ...addPropIfExists('orderId'),
22
- ...addPropIfExists('lineOfCreditApplicationId'),
23
- ...addPropIfExists('inWebView'),
10
+ ...('token' in props &&
11
+ props.token !== undefined &&
12
+ props.token !== null && { token: props.token }),
13
+ ...('partner' in props &&
14
+ props.partner !== undefined &&
15
+ props.partner !== null && { partner: props.partner }),
16
+ ...('externalBusinessId' in props &&
17
+ props.externalBusinessId !== undefined &&
18
+ props.externalBusinessId !== null && {
19
+ externalBusinessId: props.externalBusinessId,
20
+ }),
21
+ ...('orderId' in props &&
22
+ props.orderId !== undefined &&
23
+ props.orderId !== null && { orderId: props.orderId }),
24
+ ...('lineOfCreditApplicationId' in props &&
25
+ props.lineOfCreditApplicationId !== undefined &&
26
+ props.lineOfCreditApplicationId !== null && {
27
+ lineOfCreditApplicationId: props.lineOfCreditApplicationId,
28
+ }),
29
+ ...('inWebView' in props &&
30
+ props.inWebView !== undefined &&
31
+ props.inWebView !== null && { inWebView: props.inWebView.toString() }),
24
32
  ...Object.fromEntries(searchParams),
25
33
  };
26
34
  const url = `${origin}${route}?${new URLSearchParams(query).toString()}`;
27
35
  if ('openInNewTab' in props && props.openInNewTab) {
28
36
  window.open(url, '_blank');
29
- return;
37
+ return () => { }; // noop
30
38
  }
31
39
  if ('mode' in props && props.mode === 'redirect') {
32
40
  window.location.href = url;
33
- return;
41
+ return () => { }; // noop
34
42
  }
35
43
  const existingParafinDashboard = document.getElementById('parafin-dashboard');
36
44
  if (existingParafinDashboard) {
@@ -69,17 +77,25 @@ export const openParafinDashboard = (props) => {
69
77
  props.onExit?.();
70
78
  }
71
79
  };
80
+ let cleanedUp = false;
81
+ const cleanup = ({ withOnExit }) => {
82
+ if (cleanedUp)
83
+ return; // noop
84
+ cleanedUp = true;
85
+ window.removeEventListener('message', messageListener);
86
+ frame.style.opacity = '0';
87
+ setTimeout(() => {
88
+ if (withOnExit)
89
+ onExit();
90
+ dashboardTargetElement.removeChild(frame);
91
+ document.body.style.removeProperty('overflow');
92
+ }, 200);
93
+ };
72
94
  const messageListener = async (event) => {
73
95
  if (event.origin === origin) {
74
96
  switch (event.data?.message) {
75
97
  case 'close-dashboard':
76
- window.removeEventListener('message', messageListener);
77
- frame.style.opacity = '0';
78
- setTimeout(() => {
79
- onExit();
80
- dashboardTargetElement.removeChild(frame);
81
- document.body.style.removeProperty('overflow');
82
- }, 200);
98
+ cleanup({ withOnExit: true });
83
99
  break;
84
100
  case 'link-opened':
85
101
  if (props.onLinkOpened && event.data?.url && event.data?.metadata) {
@@ -94,9 +110,96 @@ export const openParafinDashboard = (props) => {
94
110
  setTimeout(() => {
95
111
  frame.style.opacity = '1';
96
112
  document.body.style.overflow = 'hidden';
97
- }, 1);
113
+ }, 0);
114
+ return () => cleanup({ withOnExit: false });
98
115
  }
99
116
  catch (error) {
100
117
  console.error('Error loading Parafin dashboard', error);
118
+ return () => { }; // noop
101
119
  }
102
120
  };
121
+ export const defaultWidgetStyles = {
122
+ width: '100%',
123
+ height: '258px',
124
+ backgroundColor: '#fff',
125
+ border: '1px solid #E8E8E8',
126
+ borderRadius: '16px',
127
+ transition: 'border 0.2s, border-radius 0.2s',
128
+ boxSizing: 'border-box',
129
+ };
130
+ export const initializeParafinWidget = (iframe, props) => {
131
+ // @ts-ignore
132
+ const url = new URL(props.widgetUrlOverride ?? 'https://widget.parafin.com');
133
+ const query = {
134
+ token: props.token,
135
+ product: props.product,
136
+ host: window.location.origin,
137
+ externalBusinessId: props.externalBusinessId ?? '',
138
+ ...Object.fromEntries(url.searchParams),
139
+ };
140
+ const iframeSrc = `${url.origin}?${new URLSearchParams(query).toString()}`;
141
+ iframe.id = `parafin-${props.product}-widget`;
142
+ iframe.src = iframeSrc;
143
+ const sendMessage = (message) => {
144
+ iframe.contentWindow?.postMessage(message, url.origin);
145
+ };
146
+ const messageListener = async ({ data, origin }) => {
147
+ if (origin === url.origin && data?.product === props.product) {
148
+ switch (data?.message) {
149
+ case 'set-border':
150
+ if (data?.borderColor) {
151
+ iframe.style.border = `1px solid ${data.borderColor}`;
152
+ }
153
+ if (data?.borderRadius) {
154
+ iframe.style.borderRadius = data.borderRadius;
155
+ }
156
+ break;
157
+ case 'open-dashboard':
158
+ openParafinDashboard({
159
+ ...props,
160
+ route: data?.route,
161
+ onExit: () => {
162
+ iframe.src = iframeSrc;
163
+ props.onExit?.();
164
+ },
165
+ });
166
+ break;
167
+ case 'person-opt-in':
168
+ if (props.onEvent) {
169
+ try {
170
+ await props.onEvent('opted_in');
171
+ sendMessage({ message: 'person-opt-in', state: 'success' });
172
+ }
173
+ catch {
174
+ sendMessage({ message: 'person-opt-in', state: 'error' });
175
+ }
176
+ }
177
+ else {
178
+ sendMessage({ message: 'person-opt-in', state: 'noop' });
179
+ }
180
+ break;
181
+ case 'person-opt-out':
182
+ if (props.onEvent) {
183
+ try {
184
+ await props.onEvent('opted_out');
185
+ sendMessage({ message: 'person-opt-out', state: 'success' });
186
+ }
187
+ catch {
188
+ sendMessage({ message: 'person-opt-out', state: 'error' });
189
+ }
190
+ }
191
+ else {
192
+ sendMessage({ message: 'person-opt-out', state: 'noop' });
193
+ }
194
+ break;
195
+ case 'set-height':
196
+ if (data?.height) {
197
+ iframe.style.height = data.height;
198
+ }
199
+ break;
200
+ }
201
+ }
202
+ };
203
+ window.addEventListener('message', messageListener);
204
+ return () => window.removeEventListener('message', messageListener);
205
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parafin/core",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "description": "Parafin embedded core",
5
5
  "author": "Parafin (https://www.parafin.com)",
6
6
  "module": "out/index.js",