@quiltt/capacitor 5.1.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.
@@ -0,0 +1,349 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react'
10
+
11
+ import type { ConnectorSDKCallbackMetadata, ConnectorSDKCallbacks } from '@quiltt/react'
12
+ import { ConnectorSDKEventType, useQuilttSession } from '@quiltt/react'
13
+
14
+ import { QuilttConnector as QuilttConnectorPlugin } from '../plugin'
15
+
16
+ export type QuilttConnectorHandle = {
17
+ handleOAuthCallback: (url: string) => void
18
+ }
19
+
20
+ type QuilttConnectorProps = {
21
+ connectorId: string
22
+ connectionId?: string
23
+ institution?: string
24
+ /**
25
+ * The app launcher URL for mobile OAuth flows.
26
+ * This URL should be a Universal Link (iOS) or App Link (Android) that redirects back to your app.
27
+ */
28
+ appLauncherUrl?: string
29
+ style?: React.CSSProperties
30
+ className?: string
31
+ } & ConnectorSDKCallbacks
32
+
33
+ const trustedQuilttHostSuffixes = ['quiltt.io', 'quiltt.dev', 'quiltt.app']
34
+
35
+ const isTrustedQuilttOrigin = (origin: string): boolean => {
36
+ try {
37
+ const originUrl = new URL(origin)
38
+ if (originUrl.protocol !== 'https:') {
39
+ return false
40
+ }
41
+
42
+ const hostname = originUrl.hostname.toLowerCase()
43
+ return trustedQuilttHostSuffixes.some(
44
+ (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`)
45
+ )
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ const decodeIfEncoded = (value: string): string => {
52
+ try {
53
+ const decoded = decodeURIComponent(value)
54
+ return decoded === value ? value : decoded
55
+ } catch {
56
+ return value
57
+ }
58
+ }
59
+
60
+ const normalizeUrlValue = (value: string): string => decodeIfEncoded(value.trim())
61
+
62
+ /**
63
+ * QuilttConnector component for Capacitor apps
64
+ * Embeds the Quiltt Connector in an iframe and handles OAuth flows via native plugins
65
+ */
66
+ export const QuilttConnector = forwardRef<QuilttConnectorHandle, QuilttConnectorProps>(
67
+ (
68
+ {
69
+ connectorId,
70
+ connectionId,
71
+ institution,
72
+ appLauncherUrl,
73
+ style,
74
+ className,
75
+ onEvent,
76
+ onLoad,
77
+ onExit,
78
+ onExitSuccess,
79
+ onExitAbort,
80
+ onExitError,
81
+ },
82
+ ref
83
+ ) => {
84
+ const iframeRef = useRef<HTMLIFrameElement>(null)
85
+ const { session } = useQuilttSession()
86
+ const [isLoaded, setIsLoaded] = useState(false)
87
+ const [loadError, setLoadError] = useState<string | null>(null)
88
+
89
+ // Connector origin for secure postMessage targeting
90
+ const connectorOrigin = useMemo(() => `https://${connectorId}.quiltt.app`, [connectorId])
91
+
92
+ // Build connector URL
93
+ const connectorUrl = useMemo(() => {
94
+ const url = new URL(connectorOrigin)
95
+
96
+ if (session?.token) {
97
+ url.searchParams.set('token', session.token)
98
+ }
99
+ if (connectionId) {
100
+ url.searchParams.set('connectionId', connectionId)
101
+ }
102
+ if (institution) {
103
+ url.searchParams.set('institution', institution)
104
+ }
105
+ if (appLauncherUrl) {
106
+ url.searchParams.set('app_launcher_url', normalizeUrlValue(appLauncherUrl))
107
+ }
108
+
109
+ if (typeof window !== 'undefined') {
110
+ url.searchParams.set('embed_location', window.location.href)
111
+ }
112
+
113
+ // Set mode for inline iframe embedding
114
+ url.searchParams.set('mode', 'INLINE')
115
+
116
+ return url.toString()
117
+ }, [connectorOrigin, session?.token, connectionId, institution, appLauncherUrl])
118
+
119
+ useEffect(() => {
120
+ setIsLoaded(false)
121
+ setLoadError(null)
122
+
123
+ const abortController = new AbortController()
124
+
125
+ const runPreflight = async () => {
126
+ try {
127
+ await fetch(connectorUrl, {
128
+ method: 'GET',
129
+ mode: 'no-cors',
130
+ credentials: 'omit',
131
+ signal: abortController.signal,
132
+ })
133
+ } catch {
134
+ setLoadError('Unable to reach Quiltt Connector. Check network and connector settings.')
135
+ }
136
+ }
137
+
138
+ void runPreflight()
139
+
140
+ return () => {
141
+ abortController.abort()
142
+ }
143
+ }, [connectorUrl])
144
+
145
+ useEffect(() => {
146
+ if (isLoaded || loadError) {
147
+ return
148
+ }
149
+
150
+ const timeoutId = window.setTimeout(() => {
151
+ setLoadError('Connector took too long to load. Please retry.')
152
+ }, 15000)
153
+
154
+ return () => {
155
+ window.clearTimeout(timeoutId)
156
+ }
157
+ }, [isLoaded, loadError])
158
+
159
+ const postOAuthCallbackToIframe = useCallback(
160
+ (callbackUrl: string) => {
161
+ if (!iframeRef.current?.contentWindow) {
162
+ return
163
+ }
164
+
165
+ const normalizedCallbackUrl = normalizeUrlValue(callbackUrl)
166
+
167
+ try {
168
+ const callback = new URL(normalizedCallbackUrl)
169
+ const params: Record<string, string> = {}
170
+ callback.searchParams.forEach((value, key) => {
171
+ params[key] = value
172
+ })
173
+
174
+ iframeRef.current.contentWindow.postMessage(
175
+ {
176
+ source: 'quiltt',
177
+ type: 'OAuthCallback',
178
+ data: {
179
+ url: normalizedCallbackUrl,
180
+ params,
181
+ },
182
+ },
183
+ connectorOrigin
184
+ )
185
+ } catch {
186
+ iframeRef.current.contentWindow.postMessage(
187
+ {
188
+ source: 'quiltt',
189
+ type: 'OAuthCallback',
190
+ data: {
191
+ url: normalizedCallbackUrl,
192
+ params: {},
193
+ },
194
+ },
195
+ connectorOrigin
196
+ )
197
+ }
198
+ },
199
+ [connectorOrigin]
200
+ )
201
+
202
+ // Handle messages from the iframe
203
+ // The platform MessageBus sends: { source: 'quiltt', type: 'Load'|'ExitSuccess'|..., ...metadata }
204
+ const handleMessage = useCallback(
205
+ (event: MessageEvent) => {
206
+ // Validate origin
207
+ if (!isTrustedQuilttOrigin(event.origin)) {
208
+ return
209
+ }
210
+
211
+ const data = event.data || {}
212
+ // Validate message is from Quiltt MessageBus
213
+ if (data.source !== 'quiltt' || !data.type) return
214
+
215
+ const { type, connectionId: msgConnectionId, profileId, connectorSession, url } = data
216
+
217
+ // Build metadata from message fields
218
+ const metadata: ConnectorSDKCallbackMetadata = {
219
+ connectorId,
220
+ ...(profileId && { profileId }),
221
+ ...(msgConnectionId && { connectionId: msgConnectionId }),
222
+ ...(connectorSession && { connectorSession }),
223
+ }
224
+
225
+ switch (type) {
226
+ case 'Load':
227
+ setIsLoaded(true)
228
+ setLoadError(null)
229
+ onEvent?.(ConnectorSDKEventType.Load, metadata)
230
+ onLoad?.(metadata)
231
+ break
232
+
233
+ case 'ExitSuccess':
234
+ onEvent?.(ConnectorSDKEventType.ExitSuccess, metadata)
235
+ onExit?.(ConnectorSDKEventType.ExitSuccess, metadata)
236
+ onExitSuccess?.(metadata)
237
+ break
238
+
239
+ case 'ExitAbort':
240
+ onEvent?.(ConnectorSDKEventType.ExitAbort, metadata)
241
+ onExit?.(ConnectorSDKEventType.ExitAbort, metadata)
242
+ onExitAbort?.(metadata)
243
+ break
244
+
245
+ case 'ExitError':
246
+ onEvent?.(ConnectorSDKEventType.ExitError, metadata)
247
+ onExit?.(ConnectorSDKEventType.ExitError, metadata)
248
+ onExitError?.(metadata)
249
+ break
250
+
251
+ case 'Navigate':
252
+ // OAuth URL - open in system browser
253
+ if (url) {
254
+ QuilttConnectorPlugin.openUrl({ url })
255
+ }
256
+ break
257
+
258
+ default:
259
+ // console.log(`Unhandled event: ${eventType}`)
260
+ break
261
+ }
262
+ },
263
+ [connectorId, onEvent, onLoad, onExit, onExitSuccess, onExitAbort, onExitError]
264
+ )
265
+
266
+ // Set up message listener
267
+ useEffect(() => {
268
+ window.addEventListener('message', handleMessage)
269
+ return () => window.removeEventListener('message', handleMessage)
270
+ }, [handleMessage])
271
+
272
+ // Listen for OAuth callbacks via deep links
273
+ useEffect(() => {
274
+ const listener = QuilttConnectorPlugin.addListener('deepLink', (event) => {
275
+ if (event.url) {
276
+ postOAuthCallbackToIframe(event.url)
277
+ }
278
+ })
279
+
280
+ // Check if app was launched with a URL
281
+ QuilttConnectorPlugin.getLaunchUrl().then((result) => {
282
+ if (result?.url) {
283
+ postOAuthCallbackToIframe(result.url)
284
+ }
285
+ })
286
+
287
+ return () => {
288
+ listener.then((l) => l.remove())
289
+ }
290
+ }, [postOAuthCallbackToIframe])
291
+
292
+ // Expose method to handle OAuth callbacks from parent component
293
+ useImperativeHandle(
294
+ ref,
295
+ () => ({
296
+ handleOAuthCallback: (callbackUrl: string) => {
297
+ postOAuthCallbackToIframe(callbackUrl)
298
+ },
299
+ }),
300
+ [postOAuthCallbackToIframe]
301
+ )
302
+
303
+ return (
304
+ <div
305
+ className={className}
306
+ style={{
307
+ width: '100%',
308
+ height: '100%',
309
+ position: 'relative',
310
+ ...style,
311
+ }}
312
+ >
313
+ <iframe
314
+ ref={iframeRef}
315
+ src={connectorUrl}
316
+ title="Quiltt Connector"
317
+ allow="publickey-credentials-get *"
318
+ style={{
319
+ border: 'none',
320
+ width: '100%',
321
+ height: '100%',
322
+ }}
323
+ onError={() => {
324
+ setLoadError('Unable to load Quiltt Connector iframe.')
325
+ }}
326
+ />
327
+
328
+ {loadError ? (
329
+ <div
330
+ style={{
331
+ position: 'absolute',
332
+ inset: 0,
333
+ display: 'flex',
334
+ alignItems: 'center',
335
+ justifyContent: 'center',
336
+ padding: '16px',
337
+ textAlign: 'center',
338
+ backgroundColor: '#fff',
339
+ }}
340
+ >
341
+ {loadError}
342
+ </div>
343
+ ) : null}
344
+ </div>
345
+ )
346
+ }
347
+ )
348
+
349
+ QuilttConnector.displayName = 'QuilttConnector'
@@ -0,0 +1 @@
1
+ export * from './QuilttConnector'
@@ -0,0 +1,81 @@
1
+ import type { PluginListenerHandle } from '@capacitor/core'
2
+
3
+ /**
4
+ * Options for opening a URL in the system browser
5
+ */
6
+ export interface OpenUrlOptions {
7
+ /**
8
+ * The URL to open in the system browser
9
+ */
10
+ url: string
11
+ }
12
+
13
+ /**
14
+ * Event data when a deep link is received
15
+ */
16
+ export interface DeepLinkEvent {
17
+ /**
18
+ * The full URL that was used to open the app, or null if no URL was present
19
+ */
20
+ url: string | null
21
+ }
22
+
23
+ /**
24
+ * Listener function for deep link events
25
+ */
26
+ export type DeepLinkListener = (event: DeepLinkEvent) => void
27
+
28
+ /**
29
+ * The Quiltt Connector Capacitor plugin interface.
30
+ *
31
+ * This plugin handles native functionality required for the Quiltt Connector:
32
+ * - Opening OAuth URLs in the system browser
33
+ * - Handling deep links / App Links / Universal Links for OAuth callbacks
34
+ */
35
+ export interface QuilttConnectorPlugin {
36
+ /**
37
+ * Open a URL in the system browser.
38
+ *
39
+ * This is used for OAuth flows where the user needs to authenticate
40
+ * with their financial institution in an external browser.
41
+ *
42
+ * @param options - The options containing the URL to open
43
+ * @returns A promise that resolves when the browser is opened
44
+ *
45
+ * @since 5.0.3
46
+ */
47
+ openUrl(options: OpenUrlOptions): Promise<{ completed: boolean }>
48
+
49
+ /**
50
+ * Get the URL that was used to launch the app, if any.
51
+ *
52
+ * This is useful for handling OAuth callbacks when the app is opened
53
+ * from a Universal Link (iOS) or App Link (Android).
54
+ *
55
+ * @returns A promise that resolves with the launch URL, or undefined if none
56
+ *
57
+ * @since 5.0.3
58
+ */
59
+ getLaunchUrl(): Promise<DeepLinkEvent>
60
+
61
+ /**
62
+ * Listen for deep link events.
63
+ *
64
+ * This is called when the app is opened via a Universal Link (iOS)
65
+ * or App Link (Android), typically during OAuth callback flows.
66
+ *
67
+ * @param eventName - The event name ('deepLink')
68
+ * @param listenerFunc - The callback function to handle the event
69
+ * @returns A promise that resolves with a handle to remove the listener
70
+ *
71
+ * @since 5.0.3
72
+ */
73
+ addListener(eventName: 'deepLink', listenerFunc: DeepLinkListener): Promise<PluginListenerHandle>
74
+
75
+ /**
76
+ * Remove all listeners for this plugin.
77
+ *
78
+ * @since 5.0.3
79
+ */
80
+ removeAllListeners(): Promise<void>
81
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @quiltt/capacitor - Framework-agnostic Capacitor plugin for Quiltt Connector
3
+ *
4
+ * This entry point provides the native Capacitor plugin without any
5
+ * framework dependencies. Works with Vue, Angular, Svelte, vanilla JS, etc.
6
+ *
7
+ * For React apps, import from '@quiltt/capacitor/react' instead.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { QuilttConnector } from '@quiltt/capacitor'
12
+ *
13
+ * // Open OAuth URL in system browser
14
+ * await QuilttConnector.openUrl({ url: 'https://...' })
15
+ *
16
+ * // Listen for deep link callbacks
17
+ * await QuilttConnector.addListener('deepLink', ({ url }) => {
18
+ * console.log('OAuth callback:', url)
19
+ * })
20
+ * ```
21
+ */
22
+
23
+ // Export type definitions
24
+ export type {
25
+ DeepLinkEvent,
26
+ DeepLinkListener,
27
+ OpenUrlOptions,
28
+ QuilttConnectorPlugin,
29
+ } from './definitions'
30
+ // Export native plugin
31
+ export { QuilttConnector } from './plugin'
package/src/plugin.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { registerPlugin } from '@capacitor/core'
2
+
3
+ import type { QuilttConnectorPlugin } from './definitions'
4
+
5
+ /**
6
+ * Native Capacitor plugin for deep link handling and URL opening
7
+ * Used internally by QuilttConnector component for OAuth flows
8
+ */
9
+ export const QuilttConnector = registerPlugin<QuilttConnectorPlugin>('QuilttConnector', {
10
+ web: () => import('./web').then((m) => new m.QuilttConnectorWeb()),
11
+ })
package/src/react.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @quiltt/capacitor/react - React components for Quiltt Connector in Capacitor apps
3
+ *
4
+ * This entry point provides React components and hooks for Capacitor apps.
5
+ * Requires React 16.8+ and @quiltt/react as peer dependencies.
6
+ *
7
+ * For non-React apps (Vue, Angular, Svelte), import from '@quiltt/capacitor' instead.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { QuilttConnector, useQuilttSession } from '@quiltt/capacitor/react'
12
+ *
13
+ * function App() {
14
+ * return (
15
+ * <QuilttConnector
16
+ * connectorId="<CONNECTOR_ID>"
17
+ * onExitSuccess={({ connectionId }) => console.log(connectionId)}
18
+ * />
19
+ * )
20
+ * }
21
+ * ```
22
+ */
23
+
24
+ // Re-export all @quiltt/react functionality for convenience
25
+ export * from '@quiltt/react'
26
+
27
+ // Export Capacitor-specific QuilttConnector component
28
+ export type { QuilttConnectorHandle } from './components'
29
+ export { QuilttConnector } from './components'
30
+ // Export plugin type definitions
31
+ export * from './definitions'
32
+ // Export native plugin for advanced use cases
33
+ export { QuilttConnector as QuilttConnectorPlugin } from './plugin'
package/src/vue.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @quiltt/capacitor/vue - Vue 3 components for Quiltt Connector in Capacitor apps
3
+ *
4
+ * This entry point provides Vue 3 components and composables for Capacitor apps.
5
+ * Requires Vue 3.3+ and @quiltt/vue as peer dependencies.
6
+ *
7
+ * For non-framework apps in plain JS (Vue, Angular, Svelte, etc.), you can also use
8
+ * the framework-agnostic import from '@quiltt/capacitor' directly.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createApp } from 'vue'
13
+ * import { QuilttPlugin } from '@quiltt/capacitor/vue'
14
+ *
15
+ * const app = createApp(App)
16
+ * app.use(QuilttPlugin, { token: '<SESSION_TOKEN>' })
17
+ * app.mount('#app')
18
+ * ```
19
+ *
20
+ * @example
21
+ * ```vue
22
+ * <script setup>
23
+ * import { QuilttConnector, useQuilttSession } from '@quiltt/capacitor/vue'
24
+ * </script>
25
+ *
26
+ * <template>
27
+ * <QuilttConnector
28
+ * connector-id="<CONNECTOR_ID>"
29
+ * @exit-success="handleSuccess"
30
+ * @navigate="handleNavigate"
31
+ * />
32
+ * </template>
33
+ * ```
34
+ */
35
+
36
+ // Re-export all @quiltt/vue functionality
37
+ export * from '@quiltt/vue'
38
+
39
+ // Export plugin type definitions
40
+ export * from './definitions'
41
+ // Export native plugin for OAuth handling
42
+ export { QuilttConnector as QuilttConnectorPlugin } from './plugin'
package/src/web.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { WebPlugin } from '@capacitor/core'
2
+
3
+ import type { DeepLinkEvent, OpenUrlOptions, QuilttConnectorPlugin } from './definitions'
4
+
5
+ /**
6
+ * Web implementation of the Quiltt Connector plugin.
7
+ *
8
+ * On web, OAuth flows typically work without special handling since
9
+ * the browser handles redirects automatically. This implementation
10
+ * provides basic functionality for web compatibility.
11
+ */
12
+ export class QuilttConnectorWeb extends WebPlugin implements QuilttConnectorPlugin {
13
+ /**
14
+ * Open a URL in a new browser tab/window.
15
+ *
16
+ * On web, this simply opens the URL in a new tab using window.open.
17
+ */
18
+ async openUrl(options: OpenUrlOptions): Promise<{ completed: boolean }> {
19
+ window.open(options.url, '_blank', 'noopener,noreferrer')
20
+ return { completed: true }
21
+ }
22
+
23
+ /**
24
+ * Get the launch URL on web.
25
+ *
26
+ * On web, we check the current URL for any OAuth callback parameters.
27
+ * This is useful when the user is redirected back to the app after OAuth.
28
+ */
29
+ async getLaunchUrl(): Promise<DeepLinkEvent> {
30
+ const currentUrl = window.location.href
31
+
32
+ // Check if the current URL contains OAuth callback parameters
33
+ // Common patterns include: code=, state=, error=
34
+ const url = new URL(currentUrl)
35
+ const hasOAuthParams =
36
+ url.searchParams.has('code') || url.searchParams.has('state') || url.searchParams.has('error')
37
+
38
+ if (hasOAuthParams) {
39
+ return { url: currentUrl }
40
+ }
41
+
42
+ return { url: null }
43
+ }
44
+ }