wallet-stack 1.0.0-alpha.138 → 1.0.0-alpha.140

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/metro-config.js CHANGED
@@ -1,9 +1,5 @@
1
- const { getDefaultConfig: getDefaultConfigExpo } = require('expo/metro-config')
2
-
3
- // Wraps Expo's getDefaultConfig to add our customizations
4
- function getDefaultConfig(...args) {
5
- const config = getDefaultConfigExpo(...args)
6
-
1
+ // Wraps a Metro config (Expo's or Sentry's) with wallet-stack customizations.
2
+ function withWalletStackConfig(config) {
7
3
  config.transformer.getTransformOptions = async () => ({
8
4
  transform: {
9
5
  experimentalImportSupport: false,
@@ -15,6 +11,7 @@ function getDefaultConfig(...args) {
15
11
  config.resolver.assetExts = [...config.resolver.assetExts, 'txt']
16
12
 
17
13
  config.resolver.extraNodeModules = {
14
+ ...config.resolver.extraNodeModules,
18
15
  // This is the crypto module we want to use moving forward (unless something better comes up).
19
16
  // It is implemented natively using OpenSSL.
20
17
  crypto: require.resolve('react-native-quick-crypto'),
@@ -23,6 +20,7 @@ function getDefaultConfig(...args) {
23
20
  buffer: require.resolve('@craftzdog/react-native-buffer'),
24
21
  }
25
22
 
23
+ const baseResolveRequest = config.resolver.resolveRequest
26
24
  // TODO: remove this once we stop using absolute imports
27
25
  config.resolver.resolveRequest = (context, moduleName, platform) => {
28
26
  if (moduleName.startsWith('src/')) {
@@ -31,10 +29,10 @@ function getDefaultConfig(...args) {
31
29
  if (moduleName === 'locales') {
32
30
  return context.resolveRequest(context, 'wallet-stack/locales', platform)
33
31
  }
34
- return context.resolveRequest(context, moduleName, platform)
32
+ return (baseResolveRequest ?? context.resolveRequest)(context, moduleName, platform)
35
33
  }
36
34
 
37
35
  return config
38
36
  }
39
37
 
40
- module.exports = { getDefaultConfig }
38
+ module.exports = { withWalletStackConfig }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallet-stack",
3
- "version": "1.0.0-alpha.138",
3
+ "version": "1.0.0-alpha.140",
4
4
  "author": "Valora Inc",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -89,12 +89,12 @@
89
89
  "@valora/react-native-keychain": "^10.0.0-valora.1",
90
90
  "@valora/react-native-webview": "^13.13.4",
91
91
  "@walletconnect/react-native-compat": "2.21.9",
92
- "expo-camera": "~16.1.11",
92
+ "expo-camera": "~17.0.10",
93
93
  "expo-splash-screen": "~0.30.10",
94
94
  "lottie-react-native": "^5.1.6",
95
95
  "mixpanel-react-native": "^3.2.1",
96
- "react": "19.0.0",
97
- "react-native": "0.79.6",
96
+ "react": "19.1.0",
97
+ "react-native": "0.81.5",
98
98
  "react-native-adjust": "^4.38.1",
99
99
  "react-native-app-control": "^1.0.2",
100
100
  "react-native-auth0": "5.0.0-beta.5",
@@ -112,10 +112,10 @@
112
112
  "react-native-permissions": "^4.1.5",
113
113
  "react-native-quick-base64": ">=2.1.0",
114
114
  "react-native-quick-crypto": "^1.0.16",
115
- "react-native-reanimated": "~3.17.4",
115
+ "react-native-reanimated": "~3.19.5",
116
116
  "react-native-restart": "^0.0.27",
117
117
  "react-native-safe-area-context": "^5.6.1",
118
- "react-native-screens": "~4.11.1",
118
+ "react-native-screens": "~4.16.0",
119
119
  "react-native-shake": "5.5.2",
120
120
  "react-native-share": "^11.1.0",
121
121
  "react-native-simple-toast": "^3.3.2",
@@ -148,8 +148,8 @@
148
148
  "bignumber.js": "^9.1.2",
149
149
  "country-data": "^0.0.31",
150
150
  "date-fns": "^4.1.0",
151
- "expo": "^53.0.22",
152
- "expo-image": "~2.4.0",
151
+ "expo": "^54.0.33",
152
+ "expo-image": "~3.0.11",
153
153
  "fast-levenshtein": "^3.0.0",
154
154
  "fp-ts": "2.16.9",
155
155
  "futoin-hkdf": "^1.5.3",
@@ -198,9 +198,9 @@
198
198
  "@tsconfig/node-lts": "^22.0.1",
199
199
  "@types/jest": "^29.5.3",
200
200
  "@types/node": "^20",
201
- "@types/react": "~19.0.10",
201
+ "@types/react": "~19.1.10",
202
202
  "@types/react-native-video": "^5.0.15",
203
- "@types/react-test-renderer": "^19.0.0",
203
+ "@types/react-test-renderer": "^19.1.0",
204
204
  "@types/redux-mock-store": "^1.0.6",
205
205
  "@walletconnect/types": "2.21.9",
206
206
  "ajv": "^8.18.0",
@@ -211,18 +211,18 @@
211
211
  "jest-junit": "^10.0.0",
212
212
  "jest-snapshot": "^29.6.2",
213
213
  "mockdate": "^3.0.5",
214
- "react": "19.0.0",
215
- "react-native": "0.79.6",
214
+ "react": "19.1.0",
215
+ "react-native": "0.81.5",
216
216
  "react-native-contacts": "https://github.com/valora-xyz/react-native-contacts#9940121",
217
217
  "react-native-kill-packager": "^1.0.0",
218
218
  "react-native-svg-mock": "^2.0.0",
219
- "react-test-renderer": "19.0.0",
219
+ "react-test-renderer": "19.1.0",
220
220
  "redux-mock-store": "^1.5.3",
221
221
  "redux-saga-test-plan": "^4.0.6",
222
222
  "rimraf": "^6.0.1",
223
223
  "ts-jest": "^29.1.1",
224
224
  "ts-node": "^11.0.0-beta.1",
225
- "typescript": "^5.8.3",
225
+ "typescript": "~5.9.2",
226
226
  "typescript-json-schema": "^0.59.0"
227
227
  },
228
228
  "resolutions": {
@@ -4,6 +4,7 @@ const config_plugins_1 = require("@expo/config-plugins");
4
4
  const withAndroidUserAgent_1 = require("./withAndroidUserAgent");
5
5
  const withAndroidWindowSoftInputModeAdjustNothing_1 = require("./withAndroidWindowSoftInputModeAdjustNothing");
6
6
  const withIosAppDelegateResetKeychain_1 = require("./withIosAppDelegateResetKeychain");
7
+ const withIosLiquidGlassCompat_1 = require("./withIosLiquidGlassCompat");
7
8
  const withIosUserAgent_1 = require("./withIosUserAgent");
8
9
  /**
9
10
  * A config plugin for configuring `wallet-stack`
@@ -13,6 +14,7 @@ const withMobileApp = (config, props = {}) => {
13
14
  // iOS
14
15
  withIosAppDelegateResetKeychain_1.withIosAppDelegateResetKeychain,
15
16
  [withIosUserAgent_1.withIosUserAgent, props],
17
+ withIosLiquidGlassCompat_1.withIosLiquidGlassCompat,
16
18
  // Android
17
19
  [withAndroidUserAgent_1.withAndroidUserAgent, props],
18
20
  withAndroidWindowSoftInputModeAdjustNothing_1.withAndroidWindowSoftInputModeAdjustNothing,
@@ -46,7 +46,7 @@ function addNeededImports(src) {
46
46
  tag: 'wallet-stack/main-application-user-agent-imports',
47
47
  src,
48
48
  newSrc: imports.join('\n'),
49
- anchor: /import com\.facebook\.soloader\.SoLoader/,
49
+ anchor: /import expo\.modules\.ReactNativeHostWrapper/,
50
50
  offset: 1,
51
51
  comment: '//',
52
52
  });
@@ -0,0 +1,14 @@
1
+ import { ConfigPlugin } from '@expo/config-plugins';
2
+ /**
3
+ * Opt the app out of iOS 26's Liquid Glass redesign by setting
4
+ * `UIDesignRequiresCompatibility` in Info.plist. The flag is ignored on
5
+ * iOS < 26, so older versions are unaffected.
6
+ *
7
+ * This is a temporary measure: Apple removes the flag in Xcode 27 and
8
+ * Liquid Glass becomes mandatory by iOS 27. Track adoption of the new
9
+ * design as tech debt and remove this mod once wallet-stack's nav bar
10
+ * styling is updated for Liquid Glass.
11
+ *
12
+ * See https://www.donnywals.com/opting-your-app-out-of-the-liquid-glass-redesign-with-xcode-26/
13
+ */
14
+ export declare const withIosLiquidGlassCompat: ConfigPlugin;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withIosLiquidGlassCompat = void 0;
4
+ const config_plugins_1 = require("@expo/config-plugins");
5
+ /**
6
+ * Opt the app out of iOS 26's Liquid Glass redesign by setting
7
+ * `UIDesignRequiresCompatibility` in Info.plist. The flag is ignored on
8
+ * iOS < 26, so older versions are unaffected.
9
+ *
10
+ * This is a temporary measure: Apple removes the flag in Xcode 27 and
11
+ * Liquid Glass becomes mandatory by iOS 27. Track adoption of the new
12
+ * design as tech debt and remove this mod once wallet-stack's nav bar
13
+ * styling is updated for Liquid Glass.
14
+ *
15
+ * See https://www.donnywals.com/opting-your-app-out-of-the-liquid-glass-redesign-with-xcode-26/
16
+ */
17
+ const withIosLiquidGlassCompat = (config) => {
18
+ return (0, config_plugins_1.withInfoPlist)(config, (config) => {
19
+ config.modResults.UIDesignRequiresCompatibility = true;
20
+ return config;
21
+ });
22
+ };
23
+ exports.withIosLiquidGlassCompat = withIosLiquidGlassCompat;
package/src/app/saga.ts CHANGED
@@ -260,9 +260,8 @@ function* watchDeepLinks() {
260
260
 
261
261
  export function* handleOpenUrl(action: OpenUrlAction) {
262
262
  const { url, openExternal, isSecureOrigin } = action
263
- const walletConnectEnabled: boolean = yield* call(isWalletConnectEnabled, url)
264
263
  Logger.debug(TAG, 'Handling url', url)
265
- if (isDeepLink(url) || (walletConnectEnabled && isWalletConnectDeepLink(url))) {
264
+ if (isDeepLink(url) || (isWalletConnectDeepLink(url) && (yield* call(isWalletConnectEnabled)))) {
266
265
  // Handle celo links directly, this avoids showing the "Open with App" sheet on Android
267
266
  yield* call(handleDeepLink, openDeepLink(url, isSecureOrigin))
268
267
  } else if (/^https?:\/\//i.test(url) === true && !openExternal) {
@@ -1,7 +1,6 @@
1
1
  import * as React from 'react'
2
- import { StyleProp, StyleSheet, ViewStyle } from 'react-native'
3
- import ReactNativeModal from 'react-native-modal'
4
- import { SafeAreaView, useSafeAreaFrame } from 'react-native-safe-area-context'
2
+ import { Modal as RNModal, Pressable, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
3
+ import { SafeAreaView } from 'react-native-safe-area-context'
5
4
  import Card from 'src/components/Card'
6
5
  import colors from 'src/styles/colors'
7
6
 
@@ -24,31 +23,51 @@ export default function Modal({
24
23
  onBackgroundPress,
25
24
  onModalHide,
26
25
  }: Props) {
27
- const { height } = useSafeAreaFrame()
26
+ // RN's <Modal> only fires `onDismiss` on iOS; emit `onModalHide` cross-platform
27
+ // whenever `isVisible` transitions from true to false.
28
+ const wasVisible = React.useRef(isVisible)
29
+ React.useEffect(() => {
30
+ if (wasVisible.current && !isVisible) {
31
+ onModalHide?.()
32
+ }
33
+ wasVisible.current = isVisible
34
+ }, [isVisible, onModalHide])
28
35
 
29
36
  return (
30
- <ReactNativeModal
31
- testID={testID}
32
- style={modalStyle}
33
- isVisible={isVisible}
34
- backdropOpacity={0.1}
35
- onBackdropPress={onBackgroundPress}
36
- // The default uses `Dimensions.get('window').height` but sometimes reports an incorrect height on Android
37
- // `useSafeAreaFrame()` seems to work better
38
- deviceHeight={height}
37
+ <RNModal
38
+ visible={isVisible}
39
+ transparent={true}
40
+ animationType="fade"
39
41
  statusBarTranslucent={true}
40
- onModalHide={onModalHide}
42
+ onRequestClose={onBackgroundPress}
43
+ testID={testID}
41
44
  >
42
- <SafeAreaView>
43
- <Card style={[styles.root, style]} rounded={true}>
44
- {children}
45
- </Card>
46
- </SafeAreaView>
47
- </ReactNativeModal>
45
+ <View style={styles.overlay}>
46
+ <Pressable style={StyleSheet.absoluteFill} onPress={onBackgroundPress} />
47
+ <View style={[styles.contentWrapper, modalStyle]} pointerEvents="box-none">
48
+ <SafeAreaView>
49
+ <Card style={[styles.root, style]} rounded={true}>
50
+ {children}
51
+ </Card>
52
+ </SafeAreaView>
53
+ </View>
54
+ </View>
55
+ </RNModal>
48
56
  )
49
57
  }
50
58
 
51
59
  const styles = StyleSheet.create({
60
+ overlay: {
61
+ flex: 1,
62
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
63
+ },
64
+ // Mirrors react-native-modal's default outer style so the card stays inset
65
+ // from screen edges and centered vertically.
66
+ contentWrapper: {
67
+ flex: 1,
68
+ margin: 20,
69
+ justifyContent: 'center',
70
+ },
52
71
  root: {
53
72
  backgroundColor: colors.backgroundPrimary,
54
73
  padding: 24,
@@ -4,13 +4,13 @@
4
4
 
5
5
  import * as React from 'react'
6
6
  import {
7
- NativeSyntheticEvent,
7
+ BlurEvent,
8
+ FocusEvent,
8
9
  Platform,
9
10
  TextInput as RNTextInput,
10
11
  TextInputProps as RNTextInputProps,
11
12
  StyleProp,
12
13
  StyleSheet,
13
- TextInputFocusEventData,
14
14
  View,
15
15
  ViewStyle,
16
16
  } from 'react-native'
@@ -46,12 +46,12 @@ export class CTextInput extends React.Component<Props, State> {
46
46
  }
47
47
  }
48
48
 
49
- handleInputFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
49
+ handleInputFocus = (e: FocusEvent) => {
50
50
  this.setState({ isFocused: true })
51
51
  this.props.onFocus?.(e)
52
52
  }
53
53
 
54
- handleInputBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
54
+ handleInputBlur = (e: BlurEvent) => {
55
55
  this.setState({ isFocused: false })
56
56
  this.props.onBlur?.(e)
57
57
  }
@@ -45,7 +45,6 @@ exports[`AccountNumber renders correctly when touch enabled 1`] = `
45
45
  onResponderTerminate={[Function]}
46
46
  onResponderTerminationRequest={[Function]}
47
47
  onStartShouldSetResponder={[Function]}
48
- ref={null}
49
48
  style={
50
49
  [
51
50
  undefined,
@@ -48,7 +48,6 @@ exports[`CircleButton renders correctly with minimum props 1`] = `
48
48
  onResponderTerminate={[Function]}
49
49
  onResponderTerminationRequest={[Function]}
50
50
  onStartShouldSetResponder={[Function]}
51
- ref={null}
52
51
  style={
53
52
  [
54
53
  [
@@ -154,7 +153,6 @@ exports[`CircleButton when given optional props renders correctly 1`] = `
154
153
  onResponderTerminate={[Function]}
155
154
  onResponderTerminationRequest={[Function]}
156
155
  onStartShouldSetResponder={[Function]}
157
- ref={null}
158
156
  style={
159
157
  [
160
158
  [
@@ -2,247 +2,218 @@
2
2
 
3
3
  exports[`renders correctly 1`] = `
4
4
  <Modal
5
- animationType="none"
6
- deviceHeight={1334}
7
- deviceWidth={null}
8
- hardwareAccelerated={false}
9
- hideModalContentWhileAnimating={false}
10
- onBackdropPress={[Function]}
11
- onModalHide={[Function]}
12
- onModalWillHide={[Function]}
13
- onModalWillShow={[Function]}
14
- onRequestClose={[Function]}
15
- panResponderThreshold={4}
16
- scrollHorizontal={false}
17
- scrollOffset={0}
18
- scrollOffsetMax={0}
19
- scrollTo={null}
5
+ animationType="fade"
20
6
  statusBarTranslucent={true}
21
- supportedOrientations={
22
- [
23
- "portrait",
24
- "landscape",
25
- ]
26
- }
27
- swipeThreshold={100}
28
7
  transparent={true}
29
8
  visible={true}
30
9
  >
31
10
  <View
32
- accessibilityState={
33
- {
34
- "busy": undefined,
35
- "checked": undefined,
36
- "disabled": undefined,
37
- "expanded": undefined,
38
- "selected": undefined,
39
- }
40
- }
41
- accessible={true}
42
- collapsable={false}
43
- focusable={true}
44
- onClick={[Function]}
45
- onResponderGrant={[Function]}
46
- onResponderMove={[Function]}
47
- onResponderRelease={[Function]}
48
- onResponderTerminate={[Function]}
49
- onResponderTerminationRequest={[Function]}
50
- onStartShouldSetResponder={[Function]}
51
- style={
52
- {
53
- "backgroundColor": "black",
54
- "bottom": 0,
55
- "height": 1334,
56
- "left": 0,
57
- "opacity": 0,
58
- "position": "absolute",
59
- "right": 0,
60
- "top": 0,
61
- "width": 750,
62
- }
63
- }
64
- />
65
- <View
66
- collapsable={false}
67
- deviceHeight={1334}
68
- deviceWidth={null}
69
- hideModalContentWhileAnimating={false}
70
- onBackdropPress={[Function]}
71
- onModalHide={[Function]}
72
- onModalWillHide={[Function]}
73
- onModalWillShow={[Function]}
74
- panResponderThreshold={4}
75
- pointerEvents="box-none"
76
- scrollHorizontal={false}
77
- scrollOffset={0}
78
- scrollOffsetMax={0}
79
- scrollTo={null}
80
- statusBarTranslucent={true}
81
11
  style={
82
12
  {
13
+ "backgroundColor": "rgba(0, 0, 0, 0.1)",
83
14
  "flex": 1,
84
- "justifyContent": "center",
85
- "margin": 37.5,
86
- "transform": [
87
- {
88
- "translateY": 1334,
89
- },
90
- ],
91
15
  }
92
16
  }
93
- supportedOrientations={
94
- [
95
- "portrait",
96
- "landscape",
97
- ]
98
- }
99
- swipeThreshold={100}
100
17
  >
101
- <RNCSafeAreaView
102
- edges={
18
+ <View
19
+ accessibilityState={
20
+ {
21
+ "busy": undefined,
22
+ "checked": undefined,
23
+ "disabled": undefined,
24
+ "expanded": undefined,
25
+ "selected": undefined,
26
+ }
27
+ }
28
+ accessibilityValue={
103
29
  {
104
- "bottom": "additive",
105
- "left": "additive",
106
- "right": "additive",
107
- "top": "additive",
30
+ "max": undefined,
31
+ "min": undefined,
32
+ "now": undefined,
33
+ "text": undefined,
108
34
  }
109
35
  }
36
+ accessible={true}
37
+ collapsable={false}
38
+ focusable={true}
39
+ onBlur={[Function]}
40
+ onClick={[Function]}
41
+ onFocus={[Function]}
42
+ onResponderGrant={[Function]}
43
+ onResponderMove={[Function]}
44
+ onResponderRelease={[Function]}
45
+ onResponderTerminate={[Function]}
46
+ onResponderTerminationRequest={[Function]}
47
+ onStartShouldSetResponder={[Function]}
48
+ style={
49
+ {
50
+ "bottom": 0,
51
+ "left": 0,
52
+ "position": "absolute",
53
+ "right": 0,
54
+ "top": 0,
55
+ }
56
+ }
57
+ />
58
+ <View
59
+ pointerEvents="box-none"
60
+ style={
61
+ [
62
+ {
63
+ "flex": 1,
64
+ "justifyContent": "center",
65
+ "margin": 20,
66
+ },
67
+ undefined,
68
+ ]
69
+ }
110
70
  >
111
- <View
112
- style={
113
- [
114
- {
115
- "backgroundColor": "#FFFFFF",
116
- "padding": 16,
117
- },
118
- {
119
- "borderRadius": 8,
120
- },
121
- {
122
- "elevation": 12,
123
- "shadowColor": "rgba(156, 164, 169, 0.4)",
124
- "shadowOffset": {
125
- "height": 2,
126
- "width": 0,
127
- },
128
- "shadowOpacity": 1,
129
- "shadowRadius": 12,
130
- },
71
+ <RNCSafeAreaView
72
+ edges={
73
+ {
74
+ "bottom": "additive",
75
+ "left": "additive",
76
+ "right": "additive",
77
+ "top": "additive",
78
+ }
79
+ }
80
+ >
81
+ <View
82
+ style={
131
83
  [
132
84
  {
133
85
  "backgroundColor": "#FFFFFF",
134
- "maxHeight": "100%",
135
- "padding": 24,
86
+ "padding": 16,
136
87
  },
137
- undefined,
138
- ],
139
- ]
140
- }
141
- >
142
- <RCTScrollView
143
- contentContainerStyle={
144
- {
145
- "alignItems": "center",
146
- }
147
- }
148
- >
149
- <View>
150
- <Text
151
- style={
152
- {
153
- "color": "#2E3338",
154
- "fontFamily": "Inter-Bold",
155
- "fontSize": 20,
156
- "lineHeight": 28,
157
- "marginBottom": 12,
158
- "textAlign": "center",
159
- }
160
- }
161
- >
162
- Dialog
163
- </Text>
164
- <Text
165
- style={
88
+ {
89
+ "borderRadius": 8,
90
+ },
91
+ {
92
+ "elevation": 12,
93
+ "shadowColor": "rgba(156, 164, 169, 0.4)",
94
+ "shadowOffset": {
95
+ "height": 2,
96
+ "width": 0,
97
+ },
98
+ "shadowOpacity": 1,
99
+ "shadowRadius": 12,
100
+ },
101
+ [
166
102
  {
167
- "color": "#2E3338",
168
- "fontFamily": "Inter-Regular",
169
- "fontSize": 16,
170
- "lineHeight": 24,
171
- "marginBottom": 24,
172
- "textAlign": "center",
173
- }
174
- }
175
- >
176
- "HELLO"
177
- </Text>
178
- </View>
179
- </RCTScrollView>
180
- <View
181
- style={
182
- {
183
- "flexDirection": "row",
184
- "flexWrap": "wrap",
185
- "justifyContent": "space-around",
186
- "maxWidth": "100%",
187
- }
103
+ "backgroundColor": "#FFFFFF",
104
+ "maxHeight": "100%",
105
+ "padding": 24,
106
+ },
107
+ undefined,
108
+ ],
109
+ ]
188
110
  }
189
111
  >
190
- <View
191
- accessibilityState={
192
- {
193
- "busy": undefined,
194
- "checked": undefined,
195
- "disabled": undefined,
196
- "expanded": undefined,
197
- "selected": undefined,
198
- }
199
- }
200
- accessibilityValue={
201
- {
202
- "max": undefined,
203
- "min": undefined,
204
- "now": undefined,
205
- "text": undefined,
206
- }
207
- }
208
- accessible={true}
209
- focusable={true}
210
- nativeBackgroundAndroid={
112
+ <RCTScrollView
113
+ contentContainerStyle={
211
114
  {
212
- "attribute": "selectableItemBackgroundBorderless",
213
- "rippleRadius": undefined,
214
- "type": "ThemeAttrAndroid",
115
+ "alignItems": "center",
215
116
  }
216
117
  }
217
- onClick={[Function]}
218
- onResponderGrant={[Function]}
219
- onResponderMove={[Function]}
220
- onResponderRelease={[Function]}
221
- onResponderTerminate={[Function]}
222
- onResponderTerminationRequest={[Function]}
223
- onStartShouldSetResponder={[Function]}
224
118
  >
225
- <Text
226
- style={
227
- [
119
+ <View>
120
+ <Text
121
+ style={
228
122
  {
229
- "color": "#1AB775",
230
- "fontFamily": "Inter-SemiBold",
123
+ "color": "#2E3338",
124
+ "fontFamily": "Inter-Bold",
125
+ "fontSize": 20,
126
+ "lineHeight": 28,
127
+ "marginBottom": 12,
128
+ "textAlign": "center",
129
+ }
130
+ }
131
+ >
132
+ Dialog
133
+ </Text>
134
+ <Text
135
+ style={
136
+ {
137
+ "color": "#2E3338",
138
+ "fontFamily": "Inter-Regular",
231
139
  "fontSize": 16,
232
140
  "lineHeight": 24,
233
- },
234
- {
235
- "paddingTop": 16,
236
- },
237
- ]
141
+ "marginBottom": 24,
142
+ "textAlign": "center",
143
+ }
144
+ }
145
+ >
146
+ "HELLO"
147
+ </Text>
148
+ </View>
149
+ </RCTScrollView>
150
+ <View
151
+ style={
152
+ {
153
+ "flexDirection": "row",
154
+ "flexWrap": "wrap",
155
+ "justifyContent": "space-around",
156
+ "maxWidth": "100%",
157
+ }
158
+ }
159
+ >
160
+ <View
161
+ accessibilityState={
162
+ {
163
+ "busy": undefined,
164
+ "checked": undefined,
165
+ "disabled": undefined,
166
+ "expanded": undefined,
167
+ "selected": undefined,
168
+ }
169
+ }
170
+ accessibilityValue={
171
+ {
172
+ "max": undefined,
173
+ "min": undefined,
174
+ "now": undefined,
175
+ "text": undefined,
176
+ }
238
177
  }
178
+ accessible={true}
179
+ focusable={true}
180
+ nativeBackgroundAndroid={
181
+ {
182
+ "attribute": "selectableItemBackgroundBorderless",
183
+ "rippleRadius": undefined,
184
+ "type": "ThemeAttrAndroid",
185
+ }
186
+ }
187
+ onClick={[Function]}
188
+ onResponderGrant={[Function]}
189
+ onResponderMove={[Function]}
190
+ onResponderRelease={[Function]}
191
+ onResponderTerminate={[Function]}
192
+ onResponderTerminationRequest={[Function]}
193
+ onStartShouldSetResponder={[Function]}
239
194
  >
240
- Press Me
241
- </Text>
195
+ <Text
196
+ style={
197
+ [
198
+ {
199
+ "color": "#1AB775",
200
+ "fontFamily": "Inter-SemiBold",
201
+ "fontSize": 16,
202
+ "lineHeight": 24,
203
+ },
204
+ {
205
+ "paddingTop": 16,
206
+ },
207
+ ]
208
+ }
209
+ >
210
+ Press Me
211
+ </Text>
212
+ </View>
242
213
  </View>
243
214
  </View>
244
- </View>
245
- </RNCSafeAreaView>
215
+ </RNCSafeAreaView>
216
+ </View>
246
217
  </View>
247
218
  </Modal>
248
219
  `;
@@ -59,7 +59,6 @@ exports[`TextInputWithButtons renders correctly 1`] = `
59
59
  onResponderTerminate={[Function]}
60
60
  onResponderTerminationRequest={[Function]}
61
61
  onStartShouldSetResponder={[Function]}
62
- ref={null}
63
62
  style={
64
63
  [
65
64
  undefined,
package/src/dapps/saga.ts CHANGED
@@ -43,8 +43,10 @@ export function* handleOpenDapp(action: PayloadAction<DappSelectedAction>) {
43
43
  ).inAppWebviewEnabled
44
44
 
45
45
  if (dappsWebViewEnabled) {
46
- const walletConnectEnabled: boolean = yield* call(isWalletConnectEnabled, dappUrl)
47
- if (isDeepLink(dappUrl) || (walletConnectEnabled && isWalletConnectDeepLink(dappUrl))) {
46
+ if (
47
+ isDeepLink(dappUrl) ||
48
+ (isWalletConnectDeepLink(dappUrl) && (yield* call(isWalletConnectEnabled)))
49
+ ) {
48
50
  yield* call(handleDeepLink, openDeepLink(dappUrl, true))
49
51
  } else {
50
52
  navigate(Screens.WebViewScreen, { uri: dappUrl })
@@ -1,5 +1,10 @@
1
+ // See useWallet.ts for why we lazy-require the internal module rather than
2
+ // using top-level runtime imports.
1
3
  import React from 'react'
2
- import BaseButton, { BtnSizes, BtnTypes, ButtonProps, TextSizes } from '../../components/Button'
4
+ import type * as InternalButton from '../../components/Button'
5
+ import type { ButtonProps } from '../../components/Button'
6
+
7
+ const loadInternal = (): typeof InternalButton => require('../../components/Button')
3
8
 
4
9
  export type ButtonSize = 'small' | 'medium' | 'full'
5
10
  export type ButtonType = 'primary' | 'secondary' | 'tertiary'
@@ -11,8 +16,9 @@ export interface CustomButtonProps extends Omit<ButtonProps, 'size' | 'type' | '
11
16
  textSize?: ButtonTextSize
12
17
  }
13
18
 
14
- function toInternalBtnType(type?: ButtonType): BtnTypes | undefined {
19
+ function toInternalBtnType(type?: ButtonType): InternalButton.BtnTypes | undefined {
15
20
  if (!type) return undefined
21
+ const { BtnTypes } = loadInternal()
16
22
  switch (type) {
17
23
  case 'primary':
18
24
  return BtnTypes.PRIMARY
@@ -26,8 +32,9 @@ function toInternalBtnType(type?: ButtonType): BtnTypes | undefined {
26
32
  }
27
33
  }
28
34
 
29
- function toInternalBtnSize(size?: ButtonSize): BtnSizes | undefined {
35
+ function toInternalBtnSize(size?: ButtonSize): InternalButton.BtnSizes | undefined {
30
36
  if (!size) return undefined
37
+ const { BtnSizes } = loadInternal()
31
38
  switch (size) {
32
39
  case 'small':
33
40
  return BtnSizes.SMALL
@@ -41,8 +48,9 @@ function toInternalBtnSize(size?: ButtonSize): BtnSizes | undefined {
41
48
  }
42
49
  }
43
50
 
44
- function toInternalTextSize(size?: ButtonTextSize): TextSizes | undefined {
51
+ function toInternalTextSize(size?: ButtonTextSize): InternalButton.TextSizes | undefined {
45
52
  if (!size) return undefined
53
+ const { TextSizes } = loadInternal()
46
54
  switch (size) {
47
55
  case 'small':
48
56
  return TextSizes.SMALL
@@ -55,6 +63,7 @@ function toInternalTextSize(size?: ButtonTextSize): TextSizes | undefined {
55
63
  }
56
64
 
57
65
  export function Button(props: CustomButtonProps) {
66
+ const { default: BaseButton } = loadInternal()
58
67
  return (
59
68
  <BaseButton
60
69
  {...props}
@@ -88,11 +88,9 @@ export function* handleQRCodeDefault({
88
88
  }: HandleQRCodeDetectedAction) {
89
89
  AppAnalytics.track(QrScreenEvents.qr_scanned, qrCode)
90
90
 
91
- const walletConnectEnabled: boolean = yield* call(isWalletConnectEnabled, qrCode.data)
92
-
93
91
  // TODO there's some duplication with deep links handing
94
92
  // would be nice to refactor this
95
- if (qrCode.data.startsWith('wc:') && walletConnectEnabled) {
93
+ if (qrCode.data.startsWith('wc:') && (yield* call(isWalletConnectEnabled))) {
96
94
  yield* fork(handleLoadingWithTimeout, WalletConnectPairingOrigin.Scan)
97
95
  yield* call(initialiseWalletConnect, qrCode.data, WalletConnectPairingOrigin.Scan)
98
96
  return
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
4
4
  import { Keyboard, TextInput as RNTextInput, StyleSheet, Text } from 'react-native'
5
5
  import { View } from 'react-native-animatable'
6
6
  import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
7
+ import Svg, { Path } from 'react-native-svg'
7
8
  import AppAnalytics from 'src/analytics/AppAnalytics'
8
9
  import { SendEvents } from 'src/analytics/Events'
9
10
  import BackButton from 'src/components/BackButton'
@@ -29,8 +30,8 @@ import { getLocalCurrencySymbol } from 'src/localCurrency/selectors'
29
30
  import { useSelector } from 'src/redux/hooks'
30
31
  import EnterAmountOptions from 'src/send/EnterAmountOptions'
31
32
  import { AmountEnteredIn } from 'src/send/types'
32
- import { typeScale } from 'src/styles/fonts'
33
33
  import Colors from 'src/styles/colors'
34
+ import { typeScale } from 'src/styles/fonts'
34
35
  import { Spacing } from 'src/styles/styles'
35
36
  import { feeCurrenciesSelector } from 'src/tokens/selectors'
36
37
  import { TokenBalance } from 'src/tokens/slice'
@@ -258,7 +259,7 @@ export default function EnterAmount({
258
259
 
259
260
  {!!recipientSlot && (
260
261
  <>
261
- <View style={styles.connectorLine} />
262
+ <ConnectorArrow />
262
263
  {recipientSlot}
263
264
  </>
264
265
  )}
@@ -367,6 +368,20 @@ export default function EnterAmount({
367
368
  )
368
369
  }
369
370
 
371
+ function ConnectorArrow() {
372
+ return (
373
+ <Svg width={12} height={16} viewBox="0 0 12 16" fill="none" style={styles.connector}>
374
+ <Path
375
+ d="M6 0 V13 M2 9 L6 13 L10 9"
376
+ stroke={Colors.borderPrimary}
377
+ strokeWidth={2}
378
+ strokeLinecap="round"
379
+ strokeLinejoin="round"
380
+ />
381
+ </Svg>
382
+ )
383
+ }
384
+
370
385
  const styles = StyleSheet.create({
371
386
  safeAreaContainer: {
372
387
  flex: 1,
@@ -394,10 +409,7 @@ const styles = StyleSheet.create({
394
409
  paddingHorizontal: Spacing.Regular16,
395
410
  borderRadius: 16,
396
411
  },
397
- connectorLine: {
398
- width: 2,
399
- height: 12,
400
- backgroundColor: Colors.borderPrimary,
412
+ connector: {
401
413
  alignSelf: 'center',
402
414
  marginVertical: Spacing.Tiny4,
403
415
  },
@@ -36,6 +36,8 @@ import {
36
36
  handlePendingState,
37
37
  initialiseWalletConnect,
38
38
  initialiseWalletConnectV2,
39
+ isWalletConnectEnabled,
40
+ isWalletConnectV2Uri,
39
41
  normalizeTransactions,
40
42
  walletConnectSaga,
41
43
  } from 'src/walletConnect/saga'
@@ -1046,6 +1048,63 @@ describe('showActionRequest', () => {
1046
1048
  const v2ConnectionString =
1047
1049
  'wc:79a02f869d0f921e435a5e0643304548ebfa4a0430f9c66fe8b1a9254db7ef77@2?relay-protocol=irn&symKey=f661b0a9316a4ce0b6892bdce42bea0f45037f2c1bee9e118a3a4bc868a32a39'
1048
1050
 
1051
+ describe('isWalletConnectV2Uri', () => {
1052
+ it('returns true for a v2 wc: URI', () => {
1053
+ expect(isWalletConnectV2Uri(v2ConnectionString)).toBe(true)
1054
+ })
1055
+
1056
+ it('returns false for any string that is not a wc: pairing URI', () => {
1057
+ // parseUri is meant for wc: URIs only; passing anything else would force it
1058
+ // down its base64 link-mode fallback, which can throw on RN's strict native
1059
+ // base64 decoder. The startsWith('wc:') guard keeps non-WC strings out.
1060
+ expect(isWalletConnectV2Uri('https://churrito.fi')).toBe(false)
1061
+ expect(isWalletConnectV2Uri('testapp://wallet/wc?uri=wc:abc@2')).toBe(false)
1062
+ expect(isWalletConnectV2Uri('')).toBe(false)
1063
+ })
1064
+
1065
+ it('returns false for a v1 wc: URI', () => {
1066
+ expect(isWalletConnectV2Uri('wc:abc@1?bridge=https%3A%2F%2Fbridge.walletconnect.org')).toBe(
1067
+ false
1068
+ )
1069
+ })
1070
+ })
1071
+
1072
+ describe('isWalletConnectEnabled', () => {
1073
+ it('returns true when project id is set and v2 is not disabled', () => {
1074
+ jest.mocked(getAppConfig).mockReturnValue({
1075
+ displayName: 'Test App',
1076
+ deepLinkUrlScheme: 'testapp',
1077
+ registryName: 'test',
1078
+ features: { walletConnect: { projectId: '123' } },
1079
+ })
1080
+ jest.mocked(getFeatureGate).mockReturnValue(false)
1081
+ expect(isWalletConnectEnabled()).toBe(true)
1082
+ })
1083
+
1084
+ it('returns false when project id is missing', () => {
1085
+ jest.mocked(getAppConfig).mockReturnValue({
1086
+ displayName: 'Test App',
1087
+ deepLinkUrlScheme: 'testapp',
1088
+ registryName: 'test',
1089
+ })
1090
+ jest.mocked(getFeatureGate).mockReturnValue(false)
1091
+ expect(isWalletConnectEnabled()).toBe(false)
1092
+ })
1093
+
1094
+ it('returns false when v2 is feature-gated off', () => {
1095
+ jest.mocked(getAppConfig).mockReturnValue({
1096
+ displayName: 'Test App',
1097
+ deepLinkUrlScheme: 'testapp',
1098
+ registryName: 'test',
1099
+ features: { walletConnect: { projectId: '123' } },
1100
+ })
1101
+ jest
1102
+ .mocked(getFeatureGate)
1103
+ .mockImplementation((gate) => gate === StatsigFeatureGates.DISABLE_WALLET_CONNECT_V2)
1104
+ expect(isWalletConnectEnabled()).toBe(false)
1105
+ })
1106
+ })
1107
+
1049
1108
  describe('initialiseWalletConnect', () => {
1050
1109
  const origin = WalletConnectPairingOrigin.Deeplink
1051
1110
  it('initializes v2 if enabled and there is a wallet connect project id', async () => {
@@ -1123,31 +1123,29 @@ export function* initialiseWalletConnectV2(uri: string, origin: WalletConnectPai
1123
1123
  yield* put(initialisePairing(uri, origin))
1124
1124
  }
1125
1125
 
1126
- export function isWalletConnectEnabled(uri: string) {
1127
- const { version } = parseUri(uri)
1126
+ export function isWalletConnectV2Uri(uri: string): boolean {
1127
+ if (!uri.startsWith('wc:')) {
1128
+ return false
1129
+ }
1130
+ return parseUri(uri).version === 2
1131
+ }
1132
+
1133
+ export function isWalletConnectEnabled(): boolean {
1128
1134
  const walletConnectV2Disabled = getFeatureGate(StatsigFeatureGates.DISABLE_WALLET_CONNECT_V2)
1129
1135
  const walletConnectProjectId = getAppConfig().features?.walletConnect?.projectId
1130
-
1131
- return !!walletConnectProjectId && !walletConnectV2Disabled && version === 2
1136
+ return !!walletConnectProjectId && !walletConnectV2Disabled
1132
1137
  }
1133
1138
 
1134
1139
  export function* initialiseWalletConnect(uri: string, origin: WalletConnectPairingOrigin) {
1135
- const walletConnectEnabled = yield* call(isWalletConnectEnabled, uri)
1136
-
1137
- const { version } = parseUri(uri)
1138
- if (!walletConnectEnabled) {
1139
- Logger.debug('initialiseWalletConnect', `v${version} is disabled, ignoring`)
1140
+ if (!isWalletConnectV2Uri(uri)) {
1141
+ Logger.debug('initialiseWalletConnect', 'URI is not a WalletConnect v2 link, ignoring')
1140
1142
  return
1141
1143
  }
1142
-
1143
- switch (version) {
1144
- case 2:
1145
- yield* call(initialiseWalletConnectV2, uri, origin)
1146
- break
1147
- case 1:
1148
- default:
1149
- throw new Error(`Unsupported WalletConnect version '${version}'`)
1144
+ if (!isWalletConnectEnabled()) {
1145
+ Logger.debug('initialiseWalletConnect', 'WalletConnect is disabled, ignoring')
1146
+ return
1150
1147
  }
1148
+ yield* call(initialiseWalletConnectV2, uri, origin)
1151
1149
  }
1152
1150
 
1153
1151
  export function* showWalletConnectionSuccessMessage(dappName: string) {
@@ -7,7 +7,6 @@ exports[`WebViewAndroidBottomSheet renders correctly when visible 1`] = `
7
7
  animationType="none"
8
8
  deviceHeight={null}
9
9
  deviceWidth={null}
10
- hardwareAccelerated={false}
11
10
  hideModalContentWhileAnimating={false}
12
11
  onBackdropPress={[MockFunction]}
13
12
  onModalHide={[Function]}