@methodfi/opal-react 0.0.2 → 1.2.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/README.md +17 -20
- package/dist/index.d.ts +50 -75
- package/dist/index.js +45 -37
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ yarn add @methodfi/opal-react
|
|
|
17
17
|
Wrap your app with the `OpalProvider`:
|
|
18
18
|
|
|
19
19
|
```tsx
|
|
20
|
-
import {OpalProvider} from '@methodfi/opal-react';
|
|
20
|
+
import { OpalProvider } from '@methodfi/opal-react';
|
|
21
21
|
|
|
22
22
|
export default function App() {
|
|
23
23
|
return (
|
|
@@ -33,37 +33,34 @@ export default function App() {
|
|
|
33
33
|
Use the `useOpal` hook in your components:
|
|
34
34
|
|
|
35
35
|
```tsx
|
|
36
|
-
import {useOpal} from '@methodfi/opal-react';
|
|
36
|
+
import { useOpal, OpalEventType } from '@methodfi/opal-react';
|
|
37
37
|
|
|
38
38
|
function Screen() {
|
|
39
|
-
const {open} = useOpal({
|
|
39
|
+
const { open, close, isOpen, error } = useOpal({
|
|
40
40
|
env: 'dev',
|
|
41
41
|
onOpen: () => {},
|
|
42
42
|
onExit: () => {},
|
|
43
|
-
onError: error => console.error('Error:', error),
|
|
44
43
|
onEvent: event => {
|
|
45
44
|
switch (event.type) {
|
|
46
|
-
case
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
case
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
case
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
case
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// See events section
|
|
45
|
+
case OpalEventType.SESSION_STARTED:
|
|
46
|
+
// ...
|
|
47
|
+
break;
|
|
48
|
+
case OpalEventType.SESSION_COMPLETED:
|
|
49
|
+
// ...
|
|
50
|
+
break;
|
|
51
|
+
case OpalEventType.SESSION_ERRORED:
|
|
52
|
+
// ...
|
|
53
|
+
break;
|
|
54
|
+
case OpalEventType.SESSION_EXITED:
|
|
55
|
+
// ...
|
|
56
|
+
break;
|
|
59
57
|
}
|
|
60
58
|
},
|
|
61
59
|
});
|
|
62
60
|
|
|
63
61
|
const onLaunchOpal = async () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
open({token});
|
|
62
|
+
const token = await getOpalToken(); // POST /opal/token
|
|
63
|
+
open({ token });
|
|
67
64
|
};
|
|
68
65
|
|
|
69
66
|
return <button onClick={onLaunchOpal}>Launch Opal</button>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,75 +1,64 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
type OpalEnvironment = 'local' | 'dev' | 'sandbox' | 'production';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
declare const OpalMode: {
|
|
5
|
+
readonly CONNECT: "connect";
|
|
6
|
+
readonly CARD_CONNECT: "card_connect";
|
|
7
|
+
readonly ACCOUNT_VERIFICATION: "account_verification";
|
|
8
|
+
readonly IDENTITY: "identity";
|
|
9
|
+
};
|
|
10
|
+
type OpalMode = (typeof OpalMode)[keyof typeof OpalMode];
|
|
11
|
+
declare const OpalEventType: {
|
|
12
|
+
readonly SESSION_STARTED: "opal.started";
|
|
13
|
+
readonly SESSION_ERRORED: "opal.errored";
|
|
14
|
+
readonly SESSION_EXITED: "opal.exited";
|
|
15
|
+
readonly SESSION_COMPLETED: "opal.completed";
|
|
16
|
+
readonly FLOW_STARTED: "flow.started";
|
|
17
|
+
readonly FLOW_COMPLETED: "flow.completed";
|
|
18
|
+
readonly STEP_STARTED: "step.started";
|
|
19
|
+
readonly STEP_COMPLETED: "step.completed";
|
|
20
|
+
};
|
|
21
|
+
type OpalEventType = (typeof OpalEventType)[keyof typeof OpalEventType];
|
|
8
22
|
/**
|
|
9
23
|
* Structured event object matching Method's Opal event format
|
|
10
|
-
*
|
|
24
|
+
* Types follow the pattern: <object>.<action>, which is
|
|
25
|
+
* also split up into those properties for convenience.
|
|
11
26
|
*/
|
|
12
27
|
interface OpalEvent {
|
|
13
|
-
/** The type of event, formatted as <mode>.<object>.<action> */
|
|
14
28
|
type: OpalEventType;
|
|
15
|
-
/** The operational mode associated with the event (e.g., 'card_connect') */
|
|
16
29
|
mode: OpalMode;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/** The action that occurred (e.g., 'started', 'verified', 'errored') */
|
|
20
|
-
action: OpalEventAction;
|
|
21
|
-
/** The ISO 8601 timestamp of when the event occurred */
|
|
30
|
+
object: string;
|
|
31
|
+
action: string;
|
|
22
32
|
timestamp: string;
|
|
23
|
-
/** Additional context or details related to the event (varies by event type) */
|
|
24
33
|
data?: OpalEventData | null;
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* Error data structure for Opal session errors
|
|
28
37
|
*/
|
|
29
38
|
interface OpalErrorData {
|
|
30
|
-
/** Error type identifier */
|
|
31
39
|
type: string;
|
|
32
|
-
/** Error sub-type for more specific categorization */
|
|
33
40
|
sub_type?: string;
|
|
34
|
-
/** Human-readable error message */
|
|
35
41
|
message: string;
|
|
36
42
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Card verification event data
|
|
39
|
-
*/
|
|
40
|
-
interface CardVerifiedEventData {
|
|
41
|
-
/** The account ID of the verified card */
|
|
42
|
-
account: string;
|
|
43
|
-
/** Additional metadata about the card verification */
|
|
44
|
-
metadata?: Record<string, any>;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Account verification event data
|
|
48
|
-
*/
|
|
49
|
-
interface AccountVerifiedEventData {
|
|
50
|
-
/** The account ID of the verified account */
|
|
51
|
-
account: string;
|
|
52
|
-
/** Account verification details */
|
|
53
|
-
account_verification?: {
|
|
54
|
-
account_id: string;
|
|
55
|
-
type: string;
|
|
56
|
-
[key: string]: any;
|
|
57
|
-
};
|
|
58
|
-
/** Additional metadata */
|
|
59
|
-
metadata?: Record<string, any>;
|
|
60
|
-
}
|
|
61
43
|
/**
|
|
62
44
|
* Union type for all possible event data structures
|
|
63
45
|
*/
|
|
64
|
-
type OpalEventData = OpalErrorData |
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
46
|
+
type OpalEventData = OpalErrorData | OpalIdentityEventData | OpalConnectEventData | OpalCardConnectEventData | OpalAccountVerificationEventData;
|
|
47
|
+
type OpalIdentityEventData = {
|
|
48
|
+
entity_id: string;
|
|
49
|
+
};
|
|
50
|
+
type OpalConnectEventData = {
|
|
51
|
+
entity_id: string;
|
|
52
|
+
accounts: string[];
|
|
53
|
+
};
|
|
54
|
+
type OpalCardConnectEventData = {
|
|
55
|
+
entity_id: string;
|
|
56
|
+
accounts: string[];
|
|
57
|
+
};
|
|
58
|
+
type OpalAccountVerificationEventData = {
|
|
59
|
+
entity_id: string;
|
|
60
|
+
accounts: string[];
|
|
61
|
+
};
|
|
73
62
|
/**
|
|
74
63
|
* Configuration object for the useOpal hook
|
|
75
64
|
*/
|
|
@@ -82,19 +71,6 @@ interface OpalConfig {
|
|
|
82
71
|
onExit?: (data?: any) => void;
|
|
83
72
|
/** Called for all Opal events with properly typed event object */
|
|
84
73
|
onEvent?: (event: OpalEvent) => void;
|
|
85
|
-
/** Called when an error occurs */
|
|
86
|
-
onError?: (error: OpalErrorData) => void;
|
|
87
|
-
/** Called on successful completion */
|
|
88
|
-
onSuccess?: (data: OpalSuccessData) => void;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Success data structure for completed Opal sessions
|
|
92
|
-
*/
|
|
93
|
-
interface OpalSuccessData {
|
|
94
|
-
/** The session ID of the completed session */
|
|
95
|
-
sessionId: string;
|
|
96
|
-
/** Additional metadata from the session */
|
|
97
|
-
metadata?: Record<string, any>;
|
|
98
74
|
}
|
|
99
75
|
/**
|
|
100
76
|
* Options for opening an Opal session
|
|
@@ -107,16 +83,10 @@ interface OpalOpenOptions {
|
|
|
107
83
|
* Return type for the useOpal hook
|
|
108
84
|
*/
|
|
109
85
|
interface UseOpalReturn {
|
|
110
|
-
/** Opens Opal with the provided token and options */
|
|
111
86
|
open: (options: OpalOpenOptions) => void;
|
|
112
|
-
/** Closes the current Opal session */
|
|
113
87
|
close: () => void;
|
|
114
|
-
/** Whether Opal is loaded and ready for interaction */
|
|
115
|
-
isReady: boolean;
|
|
116
|
-
/** Whether Opal is currently open */
|
|
117
88
|
isOpen: boolean;
|
|
118
|
-
|
|
119
|
-
error: OpalError | OpalErrorData | null;
|
|
89
|
+
error: OpalErrorData | null;
|
|
120
90
|
}
|
|
121
91
|
|
|
122
92
|
interface OpalProviderProps {
|
|
@@ -147,21 +117,26 @@ declare function OpalProvider({ children }: OpalProviderProps): JSX.Element;
|
|
|
147
117
|
*
|
|
148
118
|
* @example
|
|
149
119
|
* ```tsx
|
|
150
|
-
* const { open, close,
|
|
120
|
+
* const { open, close, isOpen, error } = useOpal({
|
|
151
121
|
* env: 'dev',
|
|
152
122
|
* onOpen: () => console.log('Opal opened'),
|
|
153
123
|
* onExit: () => console.log('Opal closed'),
|
|
154
124
|
* onEvent: (event) => {
|
|
155
125
|
* switch (event.type) {
|
|
156
|
-
* case
|
|
126
|
+
* case OpalEventType.SESSION_STARTED:
|
|
157
127
|
* console.log('Session started');
|
|
158
128
|
* break;
|
|
159
|
-
* case
|
|
160
|
-
* console.log('
|
|
129
|
+
* case OpalEventType.SESSION_COMPLETED:
|
|
130
|
+
* console.log('Session completed:', event.data);
|
|
131
|
+
* break;
|
|
132
|
+
* case OpalEventType.SESSION_EXITED:
|
|
133
|
+
* console.log('Session exited:', event.data);
|
|
134
|
+
* break;
|
|
135
|
+
* case OpalEventType.SESSION_ERRORED:
|
|
136
|
+
* console.log('Session errored:', event.data);
|
|
161
137
|
* break;
|
|
162
138
|
* }
|
|
163
139
|
* },
|
|
164
|
-
* onError: (error) => console.error('Opal error:', error),
|
|
165
140
|
* });
|
|
166
141
|
*
|
|
167
142
|
* // Open Opal with a token
|
|
@@ -176,4 +151,4 @@ declare function OpalProvider({ children }: OpalProviderProps): JSX.Element;
|
|
|
176
151
|
*/
|
|
177
152
|
declare function useOpal(config?: OpalConfig): UseOpalReturn;
|
|
178
153
|
|
|
179
|
-
export { OpalProvider, useOpal };
|
|
154
|
+
export { OpalEventType, OpalMode, OpalProvider, useOpal };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
|
|
2
|
+
import React, { useState, createContext, useEffect, useRef, useCallback, useContext } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
// Opal modes supported by the SDK
|
|
5
|
+
const OpalMode = {
|
|
6
|
+
CONNECT: 'connect',
|
|
7
|
+
CARD_CONNECT: 'card_connect',
|
|
8
|
+
ACCOUNT_VERIFICATION: 'account_verification',
|
|
9
|
+
IDENTITY: 'identity'
|
|
10
|
+
};
|
|
11
|
+
// Common Opal event types - supports card_connect and account_verification modes
|
|
12
|
+
const OpalEventType = {
|
|
13
|
+
SESSION_STARTED: 'opal.started',
|
|
14
|
+
SESSION_ERRORED: 'opal.errored',
|
|
15
|
+
SESSION_EXITED: 'opal.exited',
|
|
16
|
+
SESSION_COMPLETED: 'opal.completed',
|
|
17
|
+
FLOW_STARTED: 'flow.started',
|
|
18
|
+
FLOW_COMPLETED: 'flow.completed',
|
|
19
|
+
STEP_STARTED: 'step.started',
|
|
20
|
+
STEP_COMPLETED: 'step.completed'
|
|
21
|
+
};
|
|
9
22
|
|
|
10
23
|
const OPAL_URLS = {
|
|
11
24
|
local: 'http://localhost:3000',
|
|
@@ -14,7 +27,7 @@ const OPAL_URLS = {
|
|
|
14
27
|
production: 'https://opal.production.methodfi.com'
|
|
15
28
|
};
|
|
16
29
|
|
|
17
|
-
const OpalContext = /*#__PURE__*/
|
|
30
|
+
const OpalContext = /*#__PURE__*/ createContext(null);
|
|
18
31
|
/**
|
|
19
32
|
* Provider component that wraps your app to enable Opal functionality.
|
|
20
33
|
* This component manages the Opal iframe state and provides context to child components.
|
|
@@ -32,11 +45,10 @@ const OpalContext = /*#__PURE__*/ React.createContext(null);
|
|
|
32
45
|
* }
|
|
33
46
|
* ```
|
|
34
47
|
*/ function OpalProvider({ children }) {
|
|
35
|
-
const [isOpen, setIsOpen] =
|
|
36
|
-
const [
|
|
37
|
-
const [
|
|
38
|
-
const [
|
|
39
|
-
const [currentConfig, setCurrentConfig] = React.useState({});
|
|
48
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
49
|
+
const [error, setError] = useState(null);
|
|
50
|
+
const [currentToken, setCurrentToken] = useState(null);
|
|
51
|
+
const [currentConfig, setCurrentConfig] = useState({});
|
|
40
52
|
const environment = currentConfig.env || 'production';
|
|
41
53
|
const baseUrl = OPAL_URLS[environment];
|
|
42
54
|
const opalUrl = currentToken ? `${baseUrl}/?token=${currentToken}` : '';
|
|
@@ -47,16 +59,12 @@ const OpalContext = /*#__PURE__*/ React.createContext(null);
|
|
|
47
59
|
const { action, data } = event;
|
|
48
60
|
// Legacy support for action-based handling
|
|
49
61
|
switch(action){
|
|
50
|
-
case 'started':
|
|
51
|
-
if (!isReady) setIsReady(true);
|
|
52
|
-
break;
|
|
53
62
|
case 'exited':
|
|
54
63
|
close();
|
|
55
64
|
break;
|
|
56
65
|
case 'errored':
|
|
57
66
|
if (!error && data) {
|
|
58
67
|
setError(data);
|
|
59
|
-
currentConfig.onError == null ? void 0 : currentConfig.onError.call(currentConfig, data);
|
|
60
68
|
}
|
|
61
69
|
break;
|
|
62
70
|
}
|
|
@@ -65,13 +73,11 @@ const OpalContext = /*#__PURE__*/ React.createContext(null);
|
|
|
65
73
|
setCurrentToken(options.token);
|
|
66
74
|
setIsOpen(true);
|
|
67
75
|
setError(null);
|
|
68
|
-
setIsReady(false);
|
|
69
76
|
currentConfig.onOpen == null ? void 0 : currentConfig.onOpen.call(currentConfig);
|
|
70
77
|
};
|
|
71
78
|
const close = ()=>{
|
|
72
79
|
setTimeout(()=>{
|
|
73
80
|
setIsOpen(false);
|
|
74
|
-
setIsReady(false);
|
|
75
81
|
setCurrentToken(null);
|
|
76
82
|
currentConfig.onExit == null ? void 0 : currentConfig.onExit.call(currentConfig);
|
|
77
83
|
}, 500);
|
|
@@ -81,15 +87,14 @@ const OpalContext = /*#__PURE__*/ React.createContext(null);
|
|
|
81
87
|
};
|
|
82
88
|
const contextValue = {
|
|
83
89
|
isOpen,
|
|
84
|
-
isReady,
|
|
85
90
|
error,
|
|
86
91
|
open,
|
|
87
92
|
close,
|
|
88
93
|
updateConfig
|
|
89
94
|
};
|
|
90
|
-
return /*#__PURE__*/
|
|
95
|
+
return /*#__PURE__*/ React.createElement(OpalContext.Provider, {
|
|
91
96
|
value: contextValue
|
|
92
|
-
}, children, isOpen && currentToken && /*#__PURE__*/
|
|
97
|
+
}, children, isOpen && currentToken && /*#__PURE__*/ React.createElement(OpalIframe, {
|
|
93
98
|
token: currentToken,
|
|
94
99
|
opalUrl: opalUrl,
|
|
95
100
|
onMessage: handleMessage,
|
|
@@ -105,7 +110,7 @@ const OpalContext = /*#__PURE__*/ React.createContext(null);
|
|
|
105
110
|
}));
|
|
106
111
|
}
|
|
107
112
|
function useOpalContext() {
|
|
108
|
-
const context =
|
|
113
|
+
const context = useContext(OpalContext);
|
|
109
114
|
if (!context) {
|
|
110
115
|
throw new Error('useOpalContext must be used within an OpalProvider');
|
|
111
116
|
}
|
|
@@ -118,21 +123,26 @@ function useOpalContext() {
|
|
|
118
123
|
*
|
|
119
124
|
* @example
|
|
120
125
|
* ```tsx
|
|
121
|
-
* const { open, close,
|
|
126
|
+
* const { open, close, isOpen, error } = useOpal({
|
|
122
127
|
* env: 'dev',
|
|
123
128
|
* onOpen: () => console.log('Opal opened'),
|
|
124
129
|
* onExit: () => console.log('Opal closed'),
|
|
125
130
|
* onEvent: (event) => {
|
|
126
131
|
* switch (event.type) {
|
|
127
|
-
* case
|
|
132
|
+
* case OpalEventType.SESSION_STARTED:
|
|
128
133
|
* console.log('Session started');
|
|
129
134
|
* break;
|
|
130
|
-
* case
|
|
131
|
-
* console.log('
|
|
135
|
+
* case OpalEventType.SESSION_COMPLETED:
|
|
136
|
+
* console.log('Session completed:', event.data);
|
|
137
|
+
* break;
|
|
138
|
+
* case OpalEventType.SESSION_EXITED:
|
|
139
|
+
* console.log('Session exited:', event.data);
|
|
140
|
+
* break;
|
|
141
|
+
* case OpalEventType.SESSION_ERRORED:
|
|
142
|
+
* console.log('Session errored:', event.data);
|
|
132
143
|
* break;
|
|
133
144
|
* }
|
|
134
145
|
* },
|
|
135
|
-
* onError: (error) => console.error('Opal error:', error),
|
|
136
146
|
* });
|
|
137
147
|
*
|
|
138
148
|
* // Open Opal with a token
|
|
@@ -147,20 +157,19 @@ function useOpalContext() {
|
|
|
147
157
|
*/ function useOpal(config = {}) {
|
|
148
158
|
const context = useOpalContext();
|
|
149
159
|
// Update the provider's config when this hook's config changes
|
|
150
|
-
|
|
160
|
+
useEffect(()=>{
|
|
151
161
|
context.updateConfig(config);
|
|
152
162
|
}, []);
|
|
153
163
|
return {
|
|
154
164
|
open: context.open,
|
|
155
165
|
close: context.close,
|
|
156
|
-
isReady: context.isReady,
|
|
157
166
|
isOpen: context.isOpen,
|
|
158
167
|
error: context.error
|
|
159
168
|
};
|
|
160
169
|
}
|
|
161
170
|
function OpalIframe({ token, opalUrl, onMessage, style }) {
|
|
162
|
-
const iframeRef =
|
|
163
|
-
const handleMessage =
|
|
171
|
+
const iframeRef = useRef(null);
|
|
172
|
+
const handleMessage = useCallback((event)=>{
|
|
164
173
|
// Check if the event is from an allowed origin
|
|
165
174
|
const allowedOrigins = [
|
|
166
175
|
'https://opal.production.methodfi.com',
|
|
@@ -179,8 +188,8 @@ function OpalIframe({ token, opalUrl, onMessage, style }) {
|
|
|
179
188
|
}
|
|
180
189
|
} catch (error) {
|
|
181
190
|
onMessage({
|
|
182
|
-
type: 'opal.
|
|
183
|
-
mode:
|
|
191
|
+
type: 'opal.errored',
|
|
192
|
+
mode: event.data.mode,
|
|
184
193
|
object: 'session',
|
|
185
194
|
action: 'errored',
|
|
186
195
|
timestamp: new Date().toISOString(),
|
|
@@ -190,7 +199,7 @@ function OpalIframe({ token, opalUrl, onMessage, style }) {
|
|
|
190
199
|
}, [
|
|
191
200
|
onMessage
|
|
192
201
|
]);
|
|
193
|
-
|
|
202
|
+
useEffect(()=>{
|
|
194
203
|
// Add event listener for iframe messages
|
|
195
204
|
window.addEventListener('message', handleMessage);
|
|
196
205
|
return ()=>{
|
|
@@ -202,7 +211,7 @@ function OpalIframe({ token, opalUrl, onMessage, style }) {
|
|
|
202
211
|
if (!token || !opalUrl) {
|
|
203
212
|
return null;
|
|
204
213
|
}
|
|
205
|
-
return /*#__PURE__*/
|
|
214
|
+
return /*#__PURE__*/ React.createElement("iframe", {
|
|
206
215
|
ref: iframeRef,
|
|
207
216
|
src: opalUrl,
|
|
208
217
|
style: style,
|
|
@@ -211,5 +220,4 @@ function OpalIframe({ token, opalUrl, onMessage, style }) {
|
|
|
211
220
|
});
|
|
212
221
|
}
|
|
213
222
|
|
|
214
|
-
|
|
215
|
-
exports.useOpal = useOpal;
|
|
223
|
+
export { OpalEventType, OpalMode, OpalProvider, useOpal };
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@methodfi/opal-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "React SDK for Opal integration",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
10
|
+
"type": "module",
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "bunchee",
|
|
12
13
|
"dev": "bunchee --watch"
|