diff --git a/CHANGELOG.md b/CHANGELOG.md
index da57861..0e8ef51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Custom onLoadUrlInBrowser and onLoadUrlInBrowserError props
- uiMessageWebviewUrlScheme is no longer supported. The default scheme of the mx widgets will be used which is "mx". clientRedirectUrl has been the recommended method for redirecting from oauth for [years](https://docs.mx.com/release-notes/2022/#february)
+- The proxy property has been removed from the connect widget. Before a connect widget is rendered a widget url should be fetched and then provided to the widget.
### Changed
diff --git a/example/app.json b/example/app.json
index 521138a..89e12a2 100644
--- a/example/app.json
+++ b/example/app.json
@@ -28,7 +28,12 @@
"favicon": "./assets/images/favicon.png"
},
"plugins": [
- "expo-router",
+ [
+ "expo-router",
+ {
+ "origin": false
+ }
+ ],
[
"expo-splash-screen",
{
diff --git a/example/app/connect.tsx b/example/app/connect.tsx
deleted file mode 100644
index c483683..0000000
--- a/example/app/connect.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from "react"
-import { StyleSheet, Platform } from "react-native"
-import { SafeAreaView } from "react-native-safe-area-context"
-import * as Linking from "expo-linking"
-
-import { ConnectWidget } from "@mxenabled/react-native-widget-sdk"
-
-const baseUrl = Platform.OS === "android" ? "http://10.0.2.2:8089" : "http://localhost:8089"
-const proxy = `${baseUrl}/user/widget_urls`
-
-const styles = StyleSheet.create({
- page: {
- backgroundColor: "#ffffff",
- paddingTop: 10,
- },
-})
-
-export default function Connect() {
- const clientRedirectUrl = Linking.createURL("connect")
-
- return (
-
- {
- console.error(`SSO URL load error: ${error}`)
- }}
- onMessage={(url) => {
- console.log(`Got a message: ${url}`)
- }}
- onInvalidMessageError={(url, _error) => {
- console.log(`Got an unknown message: ${url}`)
- }}
- onLoad={(_payload) => {
- console.log("Widget is loading")
- }}
- onLoaded={(_payload) => {
- console.log("Widget has loaded")
- }}
- onMemberConnected={(payload) => {
- console.log(`Member connected with payload: ${JSON.stringify(payload)}`)
- }}
- onOAuthRequested={(payload) => {
- console.log(`OAuth requested with URL: ${payload.url}`)
- }}
- onStepChange={(payload) => {
- console.log(`Moving from ${payload.previous} to ${payload.current}`)
- }}
- onSelectedInstitution={(payload) => {
- console.log(`Selecting ${payload.name}`)
- }}
- />
-
- )
-}
diff --git a/example/package-lock.json b/example/package-lock.json
index 2bc3ae3..1e2e9ca 100644
--- a/example/package-lock.json
+++ b/example/package-lock.json
@@ -2330,9 +2330,9 @@
}
},
"node_modules/@isaacs/brace-expansion": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
- "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
+ "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
diff --git a/example/app/_layout.tsx b/example/src/app/_layout.tsx
similarity index 100%
rename from example/app/_layout.tsx
rename to example/src/app/_layout.tsx
diff --git a/example/app/budgets.tsx b/example/src/app/budgets.tsx
similarity index 100%
rename from example/app/budgets.tsx
rename to example/src/app/budgets.tsx
diff --git a/example/src/app/connect.tsx b/example/src/app/connect.tsx
new file mode 100644
index 0000000..39744ab
--- /dev/null
+++ b/example/src/app/connect.tsx
@@ -0,0 +1,66 @@
+import React, { useEffect, useState } from "react"
+import { StyleSheet } from "react-native"
+import { SafeAreaView } from "react-native-safe-area-context"
+import * as Linking from "expo-linking"
+
+import { ConnectWidget } from "@mxenabled/react-native-widget-sdk"
+import { fetchConnectWidgetUrl } from "../shared/api"
+
+const styles = StyleSheet.create({
+ page: {
+ backgroundColor: "#ffffff",
+ paddingTop: 10,
+ },
+})
+
+export default function Connect() {
+ const clientRedirectUrl = Linking.createURL("connect")
+
+ const [url, setUrl] = useState(null)
+
+ useEffect(() => {
+ fetchConnectWidgetUrl(clientRedirectUrl).then((url) => {
+ setUrl(url)
+ })
+ }, [clientRedirectUrl])
+
+ return (
+
+ {url && (
+ {
+ console.error(`SSO URL load error: ${error}`)
+ }}
+ onMessage={(url) => {
+ console.log(`Got a message: ${url}`)
+ }}
+ onInvalidMessageError={(url, _error) => {
+ console.log(`Got an unknown message: ${url}`)
+ }}
+ onLoad={(_payload) => {
+ console.log("Widget is loading")
+ }}
+ onLoaded={(_payload) => {
+ console.log("Widget has loaded")
+ }}
+ onMemberConnected={(payload) => {
+ console.log(`Member connected with payload: ${JSON.stringify(payload)}`)
+ }}
+ onOAuthError={(payload) => {
+ console.log(`OAuth error with payload: ${JSON.stringify(payload)}`)
+ }}
+ onOAuthRequested={(payload) => {
+ console.log(`OAuth requested with URL: ${payload.url}`)
+ }}
+ onStepChange={(payload) => {
+ console.log(`Moving from ${payload.previous} to ${payload.current}`)
+ }}
+ onSelectedInstitution={(payload) => {
+ console.log(`Selecting ${payload.name}`)
+ }}
+ />
+ )}
+
+ )
+}
diff --git a/example/app/goals.tsx b/example/src/app/goals.tsx
similarity index 100%
rename from example/app/goals.tsx
rename to example/src/app/goals.tsx
diff --git a/example/app/index.tsx b/example/src/app/index.tsx
similarity index 100%
rename from example/app/index.tsx
rename to example/src/app/index.tsx
diff --git a/example/app/oauth_complete.tsx b/example/src/app/oauth_complete.tsx
similarity index 100%
rename from example/app/oauth_complete.tsx
rename to example/src/app/oauth_complete.tsx
diff --git a/example/app/pulse.tsx b/example/src/app/pulse.tsx
similarity index 100%
rename from example/app/pulse.tsx
rename to example/src/app/pulse.tsx
diff --git a/example/app/spending.tsx b/example/src/app/spending.tsx
similarity index 100%
rename from example/app/spending.tsx
rename to example/src/app/spending.tsx
diff --git a/example/app/transactions.tsx b/example/src/app/transactions.tsx
similarity index 100%
rename from example/app/transactions.tsx
rename to example/src/app/transactions.tsx
diff --git a/example/src/shared/api.ts b/example/src/shared/api.ts
new file mode 100644
index 0000000..0e22201
--- /dev/null
+++ b/example/src/shared/api.ts
@@ -0,0 +1,47 @@
+import { Platform } from "react-native"
+
+const baseUrl = Platform.OS === "android" ? "http://10.0.2.2:8089" : "http://localhost:8089"
+const proxyUrl = `${baseUrl}/user/widget_urls`
+
+interface WidgetUrlResponse {
+ widget_url: {
+ url: string
+ }
+}
+
+export const fetchConnectWidgetUrl = async (clientRedirectUrl: string) => {
+ const headers: Record = {
+ "Accept-Version": "v20250224",
+ "Content-Type": "application/json",
+ }
+
+ const method = "POST"
+ const body = JSON.stringify({
+ widget_url: {
+ client_redirect_url: clientRedirectUrl,
+ is_mobile_webview: true,
+ ui_message_version: 4,
+ widget_type: "connect_widget",
+ data_request: { products: ["identity_verification"] },
+ },
+ })
+
+ try {
+ const response = await fetch(proxyUrl, {
+ body,
+ headers,
+ method,
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch widget URL: ${response.status} ${response.statusText}`)
+ }
+
+ const data: WidgetUrlResponse = await response.json()
+
+ return data.widget_url.url
+ } catch (error) {
+ console.error("Error fetching widget URL:", error)
+ throw error
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 1a237fa..2389fc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3765,9 +3765,9 @@
}
},
"node_modules/@isaacs/brace-expansion": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
- "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
+ "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
diff --git a/src/components/ConnectWidgets.test.tsx b/src/components/ConnectWidgets.test.tsx
deleted file mode 100644
index 49f09e3..0000000
--- a/src/components/ConnectWidgets.test.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { render, waitFor } from "@testing-library/react-native"
-import { ConnectWidget } from "./ConnectWidgets"
-import { fullWidgetComponentTestSuite } from "./widgets.test"
-import { triggerUrlChange } from "../../test/mocks/react_native"
-
-jest.mock("expo-web-browser", () => {
- return {
- openAuthSessionAsync: jest.fn().mockResolvedValue({ type: "success" }),
- }
-})
-
-describe("ConnectWidget", () => {
- fullWidgetComponentTestSuite(ConnectWidget)
-
- describe("OAuth", () => {
- const testCases: {
- label: string
- url: string
- check: (msg: string) => void
- }[] = [
- {
- label:
- "an OAuth deeplink triggers a post message to the web view (public URL with matching host)",
- url: "appscheme://oauth_complete?member_guid=MBR-123&status=success",
- check: (msg) => expect(msg).toContain("MBR-123"),
- },
- {
- label:
- "an OAuth deeplink triggers a post message to the web view (local URL with matching path)",
- url: "exp://127.0.0.1:19000/--/oauth_complete?member_guid=MBR-123&status=success",
- check: (msg) => expect(msg).toContain("MBR-123"),
- },
- {
- label:
- "an OAuth deeplink triggers a post message to the web view (public URL with matching path)",
- url: "exp://exp.host/@community/with-webbrowser-redirect/--/oauth_complete?member_guid=MBR-123&status=success",
- check: (msg) => expect(msg).toContain("MBR-123"),
- },
- {
- label:
- "an OAuth success deeplink includes the right status (public URL with matching host)",
- url: "appscheme://oauth_complete?member_guid=MBR-123&status=success",
- check: (msg) => expect(msg).toContain("oauthComplete/success"),
- },
- {
- label: "an OAuth success deeplink includes the right status (local URL with matching path)",
- url: "exp://127.0.0.1:19000/--/oauth_complete?member_guid=MBR-123&status=success",
- check: (msg) => expect(msg).toContain("oauthComplete/success"),
- },
- {
- label:
- "an OAuth success deeplink includes the right status (public URL with matching path)",
- url: "exp://exp.host/@community/with-webbrowser-redirect/--/oauth_complete?member_guid=MBR-123&status=success",
- check: (msg) => expect(msg).toContain("oauthComplete/success"),
- },
- {
- label:
- "an OAuth failure deeplink includes the right status (public URL with matching host)",
- url: "appscheme://oauth_complete?member_guid=MBR-123&status=error",
- check: (msg) => expect(msg).toContain("oauthComplete/error"),
- },
- {
- label: "an OAuth failure deeplink includes the right status (local URL with matching path)",
- url: "exp://127.0.0.1:19000/--/oauth_complete?member_guid=MBR-123&status=error",
- check: (msg) => expect(msg).toContain("oauthComplete/error"),
- },
- {
- label:
- "an OAuth failure deeplink includes the right status (public URL with matching path)",
- url: "exp://exp.host/@community/with-webbrowser-redirect/--/oauth_complete?member_guid=MBR-123&status=error",
- check: (msg) => expect(msg).toContain("oauthComplete/error"),
- },
- ]
-
- testCases.forEach((testCase) => {
- test(testCase.label, async () => {
- expect.assertions(1)
-
- const component = render(
- {
- testCase.check(msg)
- }}
- />,
- )
-
- await waitFor(() => component.findByTestId("widget_webview"))
- triggerUrlChange(testCase.url)
- })
- })
- })
-})
diff --git a/src/components/ConnectWidgets.tsx b/src/components/ConnectWidgets.tsx
index 3b05339..0c27b5e 100644
--- a/src/components/ConnectWidgets.tsx
+++ b/src/components/ConnectWidgets.tsx
@@ -7,12 +7,10 @@ import {
import { Type, SsoUrlProps, ConnectWidgetConfigurationProps } from "../sso"
import * as WebBrowser from "expo-web-browser"
import { makeWidgetComponentWithDefaults } from "./make_component"
-import { useOAuthDeeplink, OAuthProps } from "./oauth"
-import { useWidgetRendererWithRef, StylingProps } from "./renderer"
+import { StylingProps, useWidgetRenderer } from "./renderer"
export type ConnectWidgetProps = SsoUrlProps &
StylingProps &
- OAuthProps &
ConnectPostMessageCallbackProps &
ConnectWidgetConfigurationProps &
JSX.IntrinsicAttributes
@@ -40,12 +38,10 @@ export function ConnectWidget(props: ConnectWidgetProps) {
onOAuthRequested,
}
- const [ref, elem] = useWidgetRendererWithRef(
+ const elem = useWidgetRenderer(
{ ...modifiedProps, widgetType: Type.ConnectWidget },
dispatchConnectLocationChangeEvent,
)
- useOAuthDeeplink(ref, modifiedProps)
-
return elem
}
diff --git a/src/components/oauth.ts b/src/components/oauth.ts
deleted file mode 100644
index 2b3abf7..0000000
--- a/src/components/oauth.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useEffect, useState } from "react"
-import { parse as parseUrl, UrlWithParsedQuery } from "url"
-import { WebViewRef } from "./webview"
-import { onUrlChange } from "../platform/deeplink"
-
-/* Used when handling and parsing an OAuth deeplink. This is what the OAuth
- * flow uses to communicate state and status back to the app.
- */
-const OAuthCompleteHost = "oauth_complete"
-const OAuthStatusSuccess = "success"
-
-/* Used when sending a postMessage to the widget. This is what the app uses to
- * communicate the result of an OAuth request after it is redirected back to
- * the app.
- */
-const OAuthTypeSuccess = "oauthComplete/success"
-const OAuthTypeError = "oauthComplete/error"
-
-type OAuthSuccessRedirectEvent = {
- type: "success"
- success: boolean
- memberGuid: string
-}
-
-type OAuthErrorRedirectEvent = {
- type: "error"
- success: boolean
-}
-
-export type OAuthRedirectEvent = OAuthSuccessRedirectEvent | OAuthErrorRedirectEvent
-
-export type OAuthProps = {
- sendOAuthPostMessage?: (webViewRef: WebViewRef, msg: string) => void
-}
-
-export function useOAuthDeeplink(
- webViewRef: WebViewRef,
- props: OAuthProps,
-): OAuthRedirectEvent | null {
- const [ev, setEvent] = useState(null)
-
- useEffect(() => {
- return onUrlChange(({ url: rawUrl }) => {
- const url = parseUrl(rawUrl, true)
-
- /**
- * When the ui_message_webview_url_scheme setting is used, our backend
- * will generate a URL which looks like this:
- *
- * ://?
- *
- * For URLs like these, we need to check the host.
- *
- * However, when a client is using Expo in dev/qa mode and doesn't have
- * access to their app's scheme, they rely on the client_redirect_url
- * setting which must use the host to specify Expo's application
- * location. In this case, the redirect URL has to have the specified
- * event as part of the path:
- *
- * exp:///--/?
- *
- * For URLs like these, we need to check the path.
- */
- const isOAuthEventUrl =
- url.host === OAuthCompleteHost || url.pathname?.includes(OAuthCompleteHost)
- if (!isOAuthEventUrl) {
- return
- }
-
- const event = buildOAuthRedirectEvent(url)
- setEvent(event)
- postOAuthMessage(event, webViewRef, props)
- })
- }, [])
-
- return ev
-}
-
-function buildOAuthRedirectEvent(url: UrlWithParsedQuery): OAuthRedirectEvent {
- const success = url.query["status"] === OAuthStatusSuccess
- const memberGuid = url.query["amp;member_guid"] || url.query["member_guid"]
- if (success && typeof memberGuid === "string") {
- return { type: "success", success, memberGuid }
- }
-
- return { type: "error", success: false }
-}
-
-function postOAuthMessage(ev: OAuthRedirectEvent, webViewRef: WebViewRef, props: OAuthProps) {
- if (!webViewRef.current) {
- return
- }
-
- let type: string
- let metadata: Record
- if (ev.type === "success") {
- type = OAuthTypeSuccess
- metadata = { member_guid: ev.memberGuid }
- } else {
- type = OAuthTypeError
- metadata = {}
- }
-
- const message = JSON.stringify({ mx: true, type, metadata })
-
- if (props.sendOAuthPostMessage) {
- props.sendOAuthPostMessage(webViewRef, message)
- } else {
- webViewRef.current.postMessage(message)
- }
-}
diff --git a/src/components/renderer.tsx b/src/components/renderer.tsx
index f2016b5..cac668d 100644
--- a/src/components/renderer.tsx
+++ b/src/components/renderer.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, MutableRefObject, ReactElement } from "react"
+import React, { ReactElement } from "react"
import { StyleProp, ViewStyle, View } from "react-native"
import { WebView } from "react-native-webview"
import { Payload } from "@mxenabled/widget-post-message-definitions"
@@ -13,28 +13,18 @@ export type StylingProps = {
webViewStyle?: StyleProp
}
-type MaybeWebViewRef = MutableRefObject
type BaseProps = Props & StylingProps
export function useWidgetRenderer(
props: BaseProps,
dispatchEvent: (url: string, callbacks: BaseProps) => Payload | undefined,
): ReactElement {
- const [_ref, elem] = useWidgetRendererWithRef(props, dispatchEvent)
- return elem
-}
-
-export function useWidgetRendererWithRef(
- props: BaseProps,
- dispatchEvent: (url: string, callbacks: BaseProps) => Payload | undefined,
-): [MaybeWebViewRef, ReactElement] {
- const ref = useRef(null)
const url = useSsoUrl(props)
const fullscreenStyles = useFullscreenStyles()
const style = props.style || fullscreenStyles
if (!url) {
- return [ref, ]
+ return
}
const handler = makeRequestInterceptor(url, {
@@ -47,13 +37,11 @@ export function useWidgetRendererWithRef(
window.MXReactNativeSDKVersion = "${sdkVersion}";
`
- return [
- ref,
+ return (
(
onShouldStartLoadWithRequest={handler}
onError={props.onWebViewError}
/>
- ,
- ]
+
+ )
}