Compare commits

...

4 Commits

Author SHA1 Message Date
Ivan Vershigora
feb4a9de15
fix: patch react-native-tcp-socket onConnect 2026-05-20 17:58:24 +01:00
Ivan Vershigora
2915ac6190
fix: remove comments 2026-05-20 17:58:24 +01:00
Ivan Vershigora
f89fb49a01
fix: patch react-native-tcp-socket onConnect 2026-05-20 17:58:24 +01:00
Ivan Vershigora
d03d2ae4f8
TST: stabilize iOS e2e — fee modal blur, lazy-mount waits
Three classes of flake fixed; all reachable iOS detox tests now pass 10/10 in a row on a real device run.

screen/SelectFeeScreen.tsx
  handleCustomFeeBlur now commits a valid custom rate via navigateWithFee.
  Before, blurring the field left the SelectFee formSheet open and the
  user's typed value abandoned. Matches user expectation (typed value +
  dismissed keyboard = apply) and is required for the e2e helper to
  reach CreateTransactionButton on iOS.

tests/e2e/helperz.js
  - helperImportWallet: waitForId('SpeedMnemonicInput') after the 5
    SpeedBackdoor taps. ImportSpeed is React.lazy + Suspense, so on a
    cold launch the input wasn't mounted when replaceText fired.
  - setCustomFeeRate: split per-platform. iOS numeric keypad has no
    Return key, so tapReturnKey was a no-op and the fee sheet never
    closed; CreateTransactionButton then failed with 'View is not
    hittable' because the sheet covered it. iOS now taps by label
    'Done' (keyboard accessory) and waits for chooseFee on SendDetails
    to come back. Android path unchanged.

tests/e2e/bluewallet.spec.js
  Added waitForId('AddressInput') before the first interaction with the
  send screen in the multisig-UR and SCAN-button tests. SendDetails is
  React.lazy + Suspense; the assertions raced its mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:58:24 +01:00
6 changed files with 142 additions and 14 deletions

View File

@ -29,7 +29,7 @@ PODS:
- hermes-engine/Pre-built (= 250829098.0.10)
- hermes-engine/Pre-built (250829098.0.10)
- lottie-ios (4.6.0)
- lottie-react-native (7.3.6):
- lottie-react-native (7.3.7):
- hermes-engine
- lottie-ios (= 4.6.0)
- RCTRequired
@ -2068,7 +2068,7 @@ PODS:
- React-Core
- RNFS (2.20.0):
- React-Core
- RNGestureHandler (2.31.1):
- RNGestureHandler (2.31.2):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@ -2325,7 +2325,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- RNSVG (15.15.4):
- RNSVG (15.15.5):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@ -2346,9 +2346,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- RNSVG/common (= 15.15.4)
- RNSVG/common (= 15.15.5)
- Yoga
- RNSVG/common (15.15.4):
- RNSVG/common (15.15.5):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@ -2806,9 +2806,9 @@ SPEC CHECKSUMS:
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d
hermes-engine: 86cdbf283775c54dc008895c3eacd24a1f2a40b4
hermes-engine: 4ed74710a31e8e31f20356c641eab1d8f7d54595
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
lottie-react-native: 615e5f4651bee144ea991ad8e900630b6b3daf5d
lottie-react-native: 26b365c3d5615e87f4db048dcb151de3eb9a8e76
RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12
RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc
RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702
@ -2817,7 +2817,7 @@ SPEC CHECKSUMS:
React: e2dc35338068bbd299c66f043ae0d7f25de8499e
React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48
React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0
React-Core-prebuilt: 9e875134f667c471ab68bf9edf1661fa11b86540
React-Core-prebuilt: 3445f1028d9b206cd45c8bbb7e2427ee891f810e
React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146
React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716
React-debug: 92944dc4d89f56d640e75498266cbde557a48189
@ -2898,14 +2898,14 @@ SPEC CHECKSUMS:
ReactCodegen: 1bd7f2174582b0e142f8671735b5c906c08b72ea
ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f
ReactNativeCameraKit: 5974256fc608631c1c812710cd98abe95dae0f88
ReactNativeDependencies: 0a5c93845772e4b1c5ad065c59a859518b13a6b7
ReactNativeDependencies: 75299c281f422106c723e79dc1f6ce7ef03241be
RealmJS: 1c37c6bdfe060f4caa0f9175aa0eedb962622ee1
RNCAsyncStorage: 2ad919e88b8bc2cd80e8697ce66d04d006743283
RNCClipboard: 715fa7c6c8366f17d00f05a439ee7488f390fa5f
RNDefaultPreference: 8a089ee8ce829a66c5453e3c5434f0785499d1c3
RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNGestureHandler: 187c5c7936abf427bc4d22d6c3b1ac80ad1f63c0
RNGestureHandler: 2ff61eac036eaf89f6818bf4ed9c39771a17d134
RNHandoff: bc8af5a86853ff13b033e7ba1114c3c5b38e6385
RNKeychain: 6778b35b5bd067c322f8479526ac09b1d61f31d0
RNLocalize: f370284ea42c48f29f0d8dd3a7bcc28a04f82155
@ -2915,7 +2915,7 @@ SPEC CHECKSUMS:
RNReanimated: c4e6659e58b793885ae6da476cb514fc913e7b85
RNScreens: 01b065ded2dfe7987bcce770ff3a196be417ff41
RNShare: 2afdc1739d80ac140b2870ae81e8b2098f4599d9
RNSVG: 04044c3abcf177fd674a1a3d13097efa1adebcbe
RNSVG: 0e52210d4d43165e7e2cf9c890a9848b27e513ac
RNWatch: 28fe1f5e0c6410d45fd20925f4796fce05522e3f
RNWorklets: dd3b2cb0750090d78d85cd3b3ec0fdbeab5ce118
Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b

50
patches/README.md Normal file
View File

@ -0,0 +1,50 @@
# patches
Local patches applied to `node_modules` by [`patch-package`](https://github.com/ds300/patch-package)
on `postinstall` (see `package.json``scripts.patches`).
When upstream ships an equivalent fix, drop the patch here and bump the dependency.
---
## `react-native-tcp-socket+6.4.1.patch`
**What:** in `TcpSockets.m onConnect:`, read the socket addresses once and
emit `connect` only when both are valid; otherwise emit an `error` event
for that client.
**Why:** `onConnect:` builds an `NSDictionary` literal from
`[socket localHost]` / `[socket connectedHost]`. The socket can disconnect
between this callback being queued and run, in which case those getters
return `nil`; a dictionary literal with a `nil` value throws
`NSInvalidArgumentException`, which is uncaught and aborts the whole app
(SIGABRT). It is intermittent and was seen against Electrum TLS
connections, but is not TLS-specific.
```
NSInvalidArgumentException — attempt to insert nil object from objects[0]
-[TcpSockets onConnect:] -> -[TcpSocketClient socketDidSecure:]
→ Signal 6 (abort)
```
The `error`-event path (rather than just skipping the event) is
deliberate: skipping silently leaves the JS side waiting forever for a
`connect` callback that never arrives. Emitting `error` lets the JS
connection fail fast so the caller can retry.
**Upstream:**
- Bug: https://github.com/Rapsssito/react-native-tcp-socket/issues/197 (open)
- https://github.com/Rapsssito/react-native-tcp-socket/pull/225 (open) —
proposes the same nil guard but skips the event, which the maintainer
noted would hang JS; this patch emits `error` instead.
- https://github.com/Rapsssito/react-native-tcp-socket/pull/172 (closed) —
earlier attempt with the same error-event structure.
**Remove this patch once an upstream fix is merged and
`react-native-tcp-socket` is bumped past 6.4.1.**
---
## `react-native-notifications+5.2.2.patch`
Android `FcmToken.java` tweak (pre-existing patch, not documented here).

View File

@ -0,0 +1,54 @@
diff --git a/node_modules/react-native-tcp-socket/ios/TcpSockets.m b/node_modules/react-native-tcp-socket/ios/TcpSockets.m
index 0fe15d2..a5553c6 100644
--- a/node_modules/react-native-tcp-socket/ios/TcpSockets.m
+++ b/node_modules/react-native-tcp-socket/ios/TcpSockets.m
@@ -219,20 +219,35 @@ - (void)onWrittenData:(TcpSocketClient *)client msgId:(NSNumber *)msgId {
- (void)onConnect:(TcpSocketClient *)client {
GCDAsyncSocket *socket = [client getSocket];
- [self sendEventWithName:@"connect"
- body:@{
- @"id" : client.id,
- @"connection" : @{
- @"localAddress" : [socket localHost],
- @"localPort" :
- [NSNumber numberWithInt:[socket localPort]],
- @"remoteAddress" : [socket connectedHost],
- @"remotePort" : [NSNumber
- numberWithInt:[socket connectedPort]],
- @"remoteFamily" : [socket isIPv4] ? @"IPv4"
- : @"IPv6"
- }
- }];
+ NSString *localAddress = [socket localHost];
+ NSString *remoteAddress = [socket connectedHost];
+ // The socket can disconnect between this callback being queued and run,
+ // in which case the address getters return nil. Building the connection
+ // dictionary with a nil value throws NSInvalidArgumentException and
+ // crashes the app, so emit an error instead of the connect event -
+ // skipping silently would hang the JS side waiting for a callback.
+ if (localAddress != nil && remoteAddress != nil) {
+ [self sendEventWithName:@"connect"
+ body:@{
+ @"id" : client.id,
+ @"connection" : @{
+ @"localAddress" : localAddress,
+ @"localPort" :
+ [NSNumber numberWithInt:[socket localPort]],
+ @"remoteAddress" : remoteAddress,
+ @"remotePort" : [NSNumber
+ numberWithInt:[socket connectedPort]],
+ @"remoteFamily" : [socket isIPv4] ? @"IPv4"
+ : @"IPv6"
+ }
+ }];
+ } else {
+ [self sendEventWithName:@"error"
+ body:@{
+ @"id" : client.id,
+ @"error" : @"Socket disconnected before connect could be reported"
+ }];
+ }
}
- (void)onListen:(TcpSocketClient *)server {

View File

@ -254,8 +254,12 @@ const SelectFeeScreen = () => {
const numericValue = Number(state.customFeeValue.replace(',', '.'));
if (!state.customFeeValue || numericValue < 0) {
dispatch({ type: FeeScreenActions.CLEAR_CUSTOM_FEE });
return;
}
}, [state.customFeeValue]);
if (numericValue > 0) {
navigateWithFee(state.customFeeValue.replace(',', '.'), NetworkTransactionFeeType.CUSTOM);
}
}, [state.customFeeValue, navigateWithFee]);
const handleCustomFocus = useCallback(() => dispatch({ type: FeeScreenActions.SET_CUSTOM_FEE_FOCUSED }), []);
const handleCustomPress = useCallback(() => {

View File

@ -643,6 +643,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await waitForId('SendButton');
await element(by.id('SendButton')).tap();
await waitForId('AddressInput');
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('BitcoinAmountInput')).replaceText('0.0005');
await element(by.id('BitcoinAmountInput')).tapReturnKey();
@ -792,6 +793,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await helperCreateWallet();
await tapAndTapAgainIfElementIsNotVisible('HomeScreenScanButton', 'ScanQrBackdoorButton');
await scanText('bitcoin:bc1qzrtn3xwlunlrm0n0uu23lr00gmdx4lnlavdy75');
await waitForId('AddressInput');
await expect(element(by.id('AddressInput'))).toHaveText('bc1qzrtn3xwlunlrm0n0uu23lr00gmdx4lnlavdy75');
// now, gona import second wallet (ln) and test bip21 with both onchain and offchain present
@ -817,6 +819,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await scanText(
'lightning:lnbc1p090vrqpp5yxpd5wjtln4r874a9grkpr772cs0uyn7ayva3ypleyut7z0a4rgsdpu235hqurfdcsx7an9wf6x7undv4h8ggpgw35hqurfdchx6eff9p6nzvfc8q5scqzpgxqyz5vqcy30v2txquuh06h6946pal4dlm4hyujqv8ec3cunetf46gfydpxswedv4sr2rlg8dwpcg3fq9gah3j42373w366e6yau37t30amp5zqqftd004',
);
await waitForId('AddressInput');
await expect(element(by.id('AddressInput'))).toHaveText(
'lnbc1p090vrqpp5yxpd5wjtln4r874a9grkpr772cs0uyn7ayva3ypleyut7z0a4rgsdpu235hqurfdcsx7an9wf6x7undv4h8ggpgw35hqurfdchx6eff9p6nzvfc8q5scqzpgxqyz5vqcy30v2txquuh06h6946pal4dlm4hyujqv8ec3cunetf46gfydpxswedv4sr2rlg8dwpcg3fq9gah3j42373w366e6yau37t30amp5zqqftd004',
);
@ -836,6 +839,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await waitForId('SelectWalletsList');
await element(by.text('Imported Lightning')).tap();
await waitForId('AddressInput');
await expect(element(by.id('AddressInput'))).toHaveText(
'lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4',
); // send screen, and ln invoice is prefilled!
@ -856,6 +860,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await waitForId('SelectWalletsList');
await element(by.text('cr34t3d')).tap();
await waitForId('AddressInput');
await expect(element(by.id('AddressInput'))).toHaveText('1DamianM2k8WfNEeJmyqSe2YW1upB7UATx'); // send screen, and ONCHAIN invoice is prefilled!
await expect(element(by.id('BitcoinAmountInput'))).toHaveText('0.000001');

View File

@ -125,6 +125,7 @@ export async function helperImportWallet(importText, walletType, expectedWalletL
for (let c = 0; c < 5; c++) {
await element(by.id('SpeedBackdoor')).tap();
}
await waitForId('SpeedMnemonicInput');
await element(by.id('SpeedMnemonicInput')).replaceText(importText);
await element(by.id('SpeedWalletTypeInput')).replaceText(walletType);
if (device.getPlatform() === 'ios') {
@ -346,8 +347,22 @@ export async function setCustomFeeRate(feeRate) {
await element(by.id('feeCustomContainerButton')).tap();
await waitForId('feeCustom');
await element(by.id('feeCustom')).replaceText(String(feeRate));
await element(by.id('feeCustom')).tapReturnKey();
await waitForKeyboardToClose();
if (device.getPlatform() === 'ios') {
// iOS numeric keypad has no Return key; tap the keyboard accessory "Done" to blur
// and trigger onBlur → navigateWithFee in SelectFeeScreen.
try {
await element(by.label('Done')).atIndex(0).tap();
} catch (_) {
await element(by.id('feeCustom')).tapReturnKey();
}
// Wait for the fee modal to close (SendDetails route resumes).
await waitFor(element(by.id('chooseFee')))
.toBeVisible()
.withTimeout(10000);
} else {
await element(by.id('feeCustom')).tapReturnKey();
await waitForKeyboardToClose();
}
}
export async function goBack() {