Skip to content

Commit d36e0b0

Browse files
authored
Merge pull request Expensify#88534 from callstack-internal/fix/87757-plaid-oauth-broader-fix
Fix: 87757 plaid oauth broader fix
2 parents b02ec5d + 3ecaaf7 commit d36e0b0

6 files changed

Lines changed: 127 additions & 0 deletions

File tree

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@
640640
"rideshare",
641641
"RNCORE",
642642
"RNFS",
643+
"RNLinksdk",
643644
"rnmapbox",
644645
"RNTL",
645646
"RNVP",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# `react-native-plaid-link-sdk` patches
2+
3+
### [react-native-plaid-link-sdk+12.5.3+001+fix-oauth-hybrid-app-ios.patch](react-native-plaid-link-sdk+12.5.3+001+fix-oauth-hybrid-app-ios.patch)
4+
5+
- Reason:
6+
7+
```
8+
Fixes Plaid OAuth getting stuck in a retry loop on HybridApp iOS (e.g.
9+
Banco Santander ES). HybridApp routes the bank's universal-link return
10+
through JS Linking, so LinkKit never sees the callback URL and re-fires
11+
OAuth after its internal timeout. The patch exposes [PLKHandler
12+
resumeAfterTermination:] to JS so we can forward the URL back into the SDK.
13+
14+
The patch:
15+
1. Adds RCT_EXPORT_METHOD(continueFromRedirectUri:) to RNLinksdk.mm.
16+
2. Tracks the active module via a static sSharedInstance, set when
17+
createPlaidLink succeeds and cleared on exit/handoff, so the
18+
forwarder can resolve the correct handler.
19+
3. Adds the matching continueFromRedirectUri Spec entry to
20+
src/fabric/NativePlaidLinkModuleiOS.ts (required on New Architecture
21+
so the TurboModule codegen exposes the method to JS).
22+
```
23+
24+
- Upstream PR/issue: -
25+
- E/App issue: https://github.com/Expensify/App/issues/87757
26+
- PR introducing patch: https://github.com/Expensify/App/pull/88534
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
2+
index cca454e..a1b66d7 100644
3+
--- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
4+
+++ b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm
5+
@@ -12,6 +12,12 @@ static NSString* const kRNLinkKitEventNameKey = @"event";
6+
static NSString* const kRNLinkKitEventMetadataKey = @"metadata";
7+
static NSString* const kRNLinkKitVersionConstant = @"version";
8+
9+
+// Tracks the module instance whose linkHandler is currently active for OAuth.
10+
+// Set when createPlaidLink succeeds, cleared whenever linkHandler is nilled.
11+
+// Lets JS forward universal-link OAuth redirects back into the SDK via
12+
+// the RCT_EXPORT_METHOD continueFromRedirectUri: below.
13+
+static RNLinksdk *sSharedInstance = nil;
14+
+
15+
@interface RNLinksdk ()
16+
@property (nonatomic, strong) id<PLKHandler> linkHandler;
17+
@property (nonatomic, strong) UIViewController* presentingViewController;
18+
@@ -92,6 +98,9 @@ RCT_EXPORT_METHOD(createPlaidLink:(NSString*)token noLoadingState:(BOOL)noLoadin
19+
}
20+
strongSelf.exitCallback = nil;
21+
strongSelf.linkHandler = nil;
22+
+ if (sSharedInstance == strongSelf) {
23+
+ sSharedInstance = nil;
24+
+ }
25+
}
26+
};
27+
28+
@@ -108,6 +117,9 @@ RCT_EXPORT_METHOD(createPlaidLink:(NSString*)token noLoadingState:(BOOL)noLoadin
29+
if (strongSelf.presentingViewController == nil) {
30+
// Deallocate the handler it's no longer needed.
31+
strongSelf.linkHandler = nil;
32+
+ if (sSharedInstance == strongSelf) {
33+
+ sSharedInstance = nil;
34+
+ }
35+
}
36+
}
37+
}
38+
@@ -121,6 +133,9 @@ RCT_EXPORT_METHOD(createPlaidLink:(NSString*)token noLoadingState:(BOOL)noLoadin
39+
NSError *creationError = nil;
40+
self.linkHandler = [RNPlaidHelper createWithLinkTokenConfiguration:config error:&creationError];
41+
self.creationError = creationError;
42+
+ if (self.linkHandler) {
43+
+ sSharedInstance = self;
44+
+ }
45+
}
46+
47+
RCT_EXPORT_METHOD(open:(BOOL)fullScreen onSuccess:(RCTResponseSenderBlock)onSuccess onExit:(RCTResponseSenderBlock)onExit) {
48+
@@ -175,6 +190,15 @@ RCT_EXPORT_METHOD(open:(BOOL)fullScreen onSuccess:(RCTResponseSenderBlock)onSucc
49+
}
50+
}
51+
52+
+RCT_EXPORT_METHOD(continueFromRedirectUri:(NSString *)urlString) {
53+
+ NSURL *url = [NSURL URLWithString:urlString];
54+
+ RNLinksdk *instance = sSharedInstance;
55+
+ if (url == nil || instance == nil || instance.linkHandler == nil) {
56+
+ return;
57+
+ }
58+
+ [instance.linkHandler resumeAfterTermination:url];
59+
+}
60+
+
61+
RCT_EXPORT_METHOD(dismiss) {
62+
[self.presentingViewController dismissViewControllerAnimated:YES
63+
completion:nil];
64+
diff --git a/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts
65+
index a783aa9..a15e91f 100644
66+
--- a/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts
67+
+++ b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts
68+
@@ -12,6 +12,7 @@ export interface Spec extends TurboModule {
69+
onSuccess: (result: UnsafeObject<LinkSuccess>) => void,
70+
onExit: (error: UnsafeObject<LinkError>, result: UnsafeObject<LinkExit>) => void,
71+
): void;
72+
+ continueFromRedirectUri(urlString: string): void;
73+
dismiss(): void;
74+
submit(
75+
phoneNumber: string | undefined,

src/libs/Navigation/linkingConfig/subscribe.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {LinkingOptions} from '@react-navigation/native';
22
import {findFocusedRoute} from '@react-navigation/native';
33
import {Linking} from 'react-native';
4+
import continuePlaidOAuth from '@libs/continuePlaidOAuth';
45
import navigationRef from '@libs/Navigation/navigationRef';
56
import type {RootNavigatorParamList} from '@libs/Navigation/types';
67
import CONST from '@src/CONST';
@@ -24,6 +25,10 @@ const subscribe: LinkingOptions<RootNavigatorParamList>['subscribe'] = (listener
2425
// React Navigation would resolve to NotFound (or reset navigation away from the Plaid step)
2526
// and break the flow — keep the current screen mounted so the SDK can finish.
2627
if (url.includes(CONST.PLAID.OAUTH_REDIRECT_PATH_IOS)) {
28+
// Forward the OAuth redirect URI into the Plaid SDK so it can finalize OAuth.
29+
// Without this, the native SDK never sees the callback URL and retries OAuth in a loop
30+
// after app-to-app bank auth returns. See issue #87757.
31+
continuePlaidOAuth(url);
2732
return;
2833
}
2934
listener(url);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {TurboModuleRegistry} from 'react-native';
2+
import type {TurboModule} from 'react-native';
3+
4+
type PlaidBridge = TurboModule & {
5+
continueFromRedirectUri: (urlString: string) => void;
6+
};
7+
8+
const bridge = TurboModuleRegistry.get<PlaidBridge>('RNLinksdk');
9+
10+
function continuePlaidOAuth(url: string): void {
11+
bridge?.continueFromRedirectUri(url);
12+
}
13+
14+
export default continuePlaidOAuth;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Parameter kept for API parity with the iOS implementation (index.ios.ts)
2+
function continuePlaidOAuth(_url: string): void {
3+
// Plaid OAuth return is handled only on iOS HybridApp (see issue #87757).
4+
}
5+
6+
export default continuePlaidOAuth;

0 commit comments

Comments
 (0)