Skip to content

Commit 177b952

Browse files
committed
Feat: improves app security with passkey
1 parent e1a7a54 commit 177b952

File tree

10 files changed

+217
-4
lines changed

10 files changed

+217
-4
lines changed

ios/BitPayApp/BitPayAppDebug.entitlements

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<string>applinks:link.test.bitpay.com</string>
1313
<string>applinks:link.staging.bitpay.com</string>
1414
<string>applinks:bitpay.onelink.me</string>
15+
<string>webcredentials:bitpay.com</string>
1516
</array>
1617
<key>com.apple.developer.in-app-payments</key>
1718
<array>

ios/BitPayApp/BitPayAppRelease.entitlements

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<string>applinks:link.test.bitpay.com</string>
1313
<string>applinks:link.staging.bitpay.com</string>
1414
<string>applinks:bitpay.onelink.me</string>
15+
<string>webcredentials:bitpay.com</string>
1516
</array>
1617
<key>com.apple.developer.in-app-payments</key>
1718
<array>
@@ -22,6 +23,6 @@
2223
<string>$(TeamIdentifierPrefix)*</string>
2324
</array>
2425
<key>com.apple.developer.payment-pass-provisioning</key>
25-
<true/>
26+
<true/>
2627
</dict>
2728
</plist>

ios/Podfile.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ PODS:
104104
- React-RCTText (= 0.79.6)
105105
- React-RCTVibration (= 0.79.6)
106106
- React-callinvoker (0.79.6)
107+
- React-Codegen (0.1.0)
107108
- React-Core (0.79.6):
108109
- glog
109110
- hermes-engine
@@ -1529,6 +1530,13 @@ PODS:
15291530
- ReactCommon/turbomodule/bridging
15301531
- ReactCommon/turbomodule/core
15311532
- Yoga
1533+
- react-native-passkey (3.1.0):
1534+
- RCT-Folly
1535+
- RCTRequired
1536+
- RCTTypeSafety
1537+
- React-Codegen
1538+
- React-Core
1539+
- ReactCommon/turbomodule/core
15321540
- react-native-print (0.11.0):
15331541
- React-Core
15341542
- react-native-randombytes (3.6.1):
@@ -2556,6 +2564,7 @@ DEPENDENCIES:
25562564
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
25572565
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
25582566
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
2567+
- react-native-passkey (from `../node_modules/react-native-passkey`)
25592568
- react-native-print (from `../node_modules/react-native-print`)
25602569
- react-native-randombytes (from `../node_modules/react-native-randombytes`)
25612570
- react-native-render-html (from `../node_modules/react-native-render-html`)
@@ -2633,6 +2642,7 @@ SPEC REPOS:
26332642
- libwebp
26342643
- Mixpanel-swift
26352644
- MultiplatformBleAdapter
2645+
- React-Codegen
26362646
- SDWebImage
26372647
- SDWebImageWebPCoder
26382648
- SocketRocket
@@ -2747,6 +2757,8 @@ EXTERNAL SOURCES:
27472757
:path: "../node_modules/@react-native-community/netinfo"
27482758
react-native-pager-view:
27492759
:path: "../node_modules/react-native-pager-view"
2760+
react-native-passkey:
2761+
:path: "../node_modules/react-native-passkey"
27502762
react-native-print:
27512763
:path: "../node_modules/react-native-print"
27522764
react-native-randombytes:
@@ -2905,6 +2917,7 @@ SPEC CHECKSUMS:
29052917
RCTTypeSafety: 7c0b654b92ef732fffc2a3992a02d10dc8f94bfd
29062918
React: bc28da5a227fa5e7b43e7ed68061f34740d4c880
29072919
React-callinvoker: b78b18b44bc2c6634f7e594ad4fd206e624d41e3
2920+
React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a
29082921
React-Core: a4a66899e0bc30cc8c0678a267356d03045e8995
29092922
React-CoreModules: 2245b5abec9edda265e5506264a40458004d0e0a
29102923
React-cxxreact: 4d3d983512548e7c9e465c838c9339c92e724f77
@@ -2943,6 +2956,7 @@ SPEC CHECKSUMS:
29432956
react-native-mmkv: daba9abc1723d9a88285ff09d29fcbd643591678
29442957
react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
29452958
react-native-pager-view: e0a6e9be6bf1d8ee1fdbaeab5c6a118ac13cefee
2959+
react-native-passkey: e0b0f58fa9b33c424bafaa0a93d66fb0e21dc092
29462960
react-native-print: f704aef52d931bfce6d1d84351dbb5232d7ecb89
29472961
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
29482962
react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"react-native-modal": "14.0.0-rc.1",
139139
"react-native-os": "1.2.6",
140140
"react-native-pager-view": "6.7.1",
141+
"react-native-passkey": "3.1.0",
141142
"react-native-permissions": "5.4.0",
142143
"react-native-print": "0.11.0",
143144
"react-native-progress": "5.0.0",

src/Root.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ import ExternalServicesSettingsGroup, {
6969
import AboutGroup, {
7070
AboutGroupParamList,
7171
} from './navigation/tabs/settings/about/AboutGroup';
72+
import SecurityGroup, {
73+
SecurityGroupParamList,
74+
} from './navigation/tabs/settings/security/SecurityGroup';
7275
import AuthGroup, {AuthGroupParamList} from './navigation/auth/AuthGroup';
7376
import BuyCryptoGroup, {
7477
BuyCryptoGroupParamList,
@@ -190,7 +193,8 @@ export type RootStackParamList = {
190193
BillGroupParamList &
191194
WalletGroupParamList &
192195
ZenLedgerGroupParamsList &
193-
SettingsGroupParamList;
196+
SettingsGroupParamList &
197+
SecurityGroupParamList;
194198

195199
// ROOT NAVIGATION CONFIG
196200
export enum RootStacks {
@@ -221,7 +225,8 @@ export type NavScreenParams = NavigatorScreenParams<
221225
NotificationsSettingsGroupParamsList &
222226
ZenLedgerGroupParamsList &
223227
NetworkFeePolicySettingsGroupParamsList &
224-
SettingsGroupParamList
228+
SettingsGroupParamList &
229+
SecurityGroupParamList
225230
>;
226231

227232
declare global {
@@ -1039,6 +1044,7 @@ export default () => {
10391044
{SwapCryptoGroup({SwapCrypto: Root, theme})}
10401045
{WalletConnectGroup({WalletConnect: Root, theme})}
10411046
{ZenLedgerGroup({ZenLedger: Root, theme})}
1047+
{SecurityGroup({Security: Root, theme})}
10421048
</Root.Navigator>
10431049
<OnGoingProcessModal />
10441050
<InAppNotification />

src/navigation/tabs/settings/components/Security.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import {Midnight, SlateDark, White} from '../../../../styles/colors';
2828
import {H4, Paragraph} from '../../../../components/styled/Text';
2929
import {useTranslation} from 'react-i18next';
3030
import {sleep} from '../../../../utils/helper-methods';
31-
import {LogActions} from '../../../../store/log';
3231
import {useLogger} from '../../../../utils/hooks';
3332
import {TouchableOpacity} from '@components/base/TouchableOpacity';
33+
import {usePasskeySupport} from '../../../../utils/usePasskeySupport';
34+
import {useNavigation} from '@react-navigation/native';
35+
import AngleRight from '../../../../../assets/img/angle-right.svg';
3436

3537
const FingerprintSvg = {
3638
light: <FingerprintImg />,
@@ -80,10 +82,13 @@ const ImgRow = styled.View`
8082

8183
const Security = () => {
8284
const {t} = useTranslation();
85+
const navigation = useNavigation();
8386
const dispatch = useDispatch();
8487
const themeType = useThemeType();
8588
const logger = useLogger();
8689
const [modalVisible, setModalVisible] = useState(false);
90+
const passkeySupported = usePasskeySupport();
91+
console.log('##### passkeySupported', passkeySupported);
8792

8893
const pinLockActive = useSelector(({APP}: RootState) => APP.pinLockActive);
8994
const biometricLockActive = useSelector(
@@ -190,9 +195,20 @@ const Security = () => {
190195
break;
191196
}
192197
};
198+
199+
const onPressPasskeys = () => {
200+
navigation.navigate('Passkeys');
201+
};
202+
193203
return (
194204
<>
195205
<SettingsComponent>
206+
{passkeySupported && (
207+
<Setting onPress={onPressPasskeys}>
208+
<SettingTitle>{t('Passkeys')}</SettingTitle>
209+
<AngleRight />
210+
</Setting>
211+
)}
196212
<Setting onPress={onPressLockButton}>
197213
<SettingTitle>{t('Lock App')}</SettingTitle>
198214
<Button onPress={onPressLockButton} buttonType={'pill'}>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import {Theme} from '@react-navigation/native';
3+
import {useTranslation} from 'react-i18next';
4+
import PasskeyScreen from './screens/Passkeys';
5+
import {Root} from '../../../../Root';
6+
import {useStackScreenOptions} from '../../../utils/headerHelpers';
7+
8+
interface SecurityProps {
9+
Security: typeof Root;
10+
theme: Theme;
11+
}
12+
13+
export type SecurityGroupParamList = {
14+
Passkeys: undefined;
15+
};
16+
17+
export enum SecurityScreens {
18+
PASSKEYS = 'Passkeys',
19+
}
20+
21+
const SecurityGroup: React.FC<SecurityProps> = ({Security, theme}) => {
22+
const commonOptions = useStackScreenOptions(theme);
23+
const {t} = useTranslation();
24+
return (
25+
<Security.Group screenOptions={commonOptions}>
26+
<Security.Screen
27+
name={SecurityScreens.PASSKEYS}
28+
component={PasskeyScreen}
29+
options={{
30+
headerTitle: t('Storage Usage'),
31+
}}
32+
/>
33+
</Security.Group>
34+
);
35+
};
36+
37+
export default SecurityGroup;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, {useMemo, useState} from 'react';
2+
import styled from 'styled-components/native';
3+
import {Platform} from 'react-native';
4+
import {SettingsComponent, SettingsContainer} from '../../SettingsRoot';
5+
import {
6+
Hr,
7+
ScreenGutter,
8+
Setting,
9+
SettingTitle,
10+
} from '../../../../../components/styled/Containers';
11+
import Button from '../../../../../components/button/Button';
12+
import {useTranslation} from 'react-i18next';
13+
import {LogActions} from '../../../../../store/log';
14+
import {Black, Feather, LightBlack, White} from '../../../../../styles/colors';
15+
import {useAppDispatch, useAppSelector} from '../../../../../utils/hooks';
16+
17+
const ScrollContainer = styled.ScrollView``;
18+
19+
const HeaderTitle = styled(Setting)`
20+
margin-top: 20px;
21+
background-color: ${({theme: {dark}}) => (dark ? LightBlack : Feather)};
22+
padding: 0 ${ScreenGutter};
23+
border-bottom-width: 1px;
24+
border-bottom-color: ${({theme: {dark}}) => (dark ? Black : White)};
25+
`;
26+
27+
const PasskeyScreen: React.FC = () => {
28+
const {t} = useTranslation();
29+
const dispatch = useAppDispatch();
30+
31+
const createPasskey = () => {
32+
// Functionality to create a new passkey goes here
33+
};
34+
35+
return (
36+
<SettingsContainer>
37+
<ScrollContainer>
38+
<HeaderTitle>
39+
<SettingTitle>{t('Setup a passkey')}</SettingTitle>
40+
</HeaderTitle>
41+
<SettingsComponent>
42+
<Setting>
43+
<SettingTitle>
44+
Passkeys are encrypted digital keys you create using your
45+
fingerprint, face, or screen lock.
46+
</SettingTitle>
47+
</Setting>
48+
</SettingsComponent>
49+
<Button buttonType={'link'} onPress={createPasskey}>
50+
Create a new Passkey
51+
</Button>
52+
</ScrollContainer>
53+
</SettingsContainer>
54+
);
55+
};
56+
57+
export default PasskeyScreen;

src/utils/usePasskeySupport.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {useEffect, useState} from 'react';
2+
import {AppState} from 'react-native';
3+
import {
4+
Passkey,
5+
PasskeyCreateRequest,
6+
PasskeyCreateResult,
7+
PasskeyGetRequest,
8+
PasskeyGetResult,
9+
} from 'react-native-passkey';
10+
11+
export function usePasskeySupport() {
12+
const [supported, setSupported] = useState<boolean>(Passkey.isSupported());
13+
14+
useEffect(() => {
15+
const sub = AppState.addEventListener('change', s => {
16+
if (s === 'active') setSupported(Passkey.isSupported());
17+
});
18+
return () => sub.remove();
19+
}, []);
20+
21+
return supported;
22+
}
23+
24+
type Json = Record<string, any>;
25+
26+
async function post<T = any>(
27+
url: string,
28+
body?: Json,
29+
token?: string,
30+
): Promise<T> {
31+
const r = await fetch(url, {
32+
method: 'POST',
33+
headers: {
34+
'content-type': 'application/json',
35+
...(token ? {authorization: `Bearer ${token}`} : {}),
36+
},
37+
body: body ? JSON.stringify(body) : undefined,
38+
});
39+
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
40+
return r.json();
41+
}
42+
43+
export async function registerPasskey(baseUrl: string, authToken: string) {
44+
console.log('#### registerPasskey');
45+
const creationOptions: PasskeyCreateRequest = await post(
46+
`${baseUrl}/webauthn/registration/options`,
47+
{},
48+
authToken,
49+
);
50+
console.log('#### creationOptions', creationOptions);
51+
52+
const result: PasskeyCreateResult = await Passkey.create(creationOptions);
53+
console.log('#### result', result);
54+
55+
// verify with server
56+
await post(`${baseUrl}/webauthn/registration/verify`, result, authToken);
57+
return true;
58+
}
59+
60+
export async function signInWithPasskey(baseUrl: string, username?: string) {
61+
console.log('#### signInWithPasskey');
62+
const requestOptions: PasskeyGetRequest = await post(
63+
`${baseUrl}/webauthn/login/options`,
64+
{username},
65+
);
66+
console.log('#### requestOptions', requestOptions);
67+
68+
const result: PasskeyGetResult = await Passkey.get(requestOptions);
69+
console.log('#### result', result);
70+
71+
// verify assertion → receive app session / JWT from your backend
72+
const session = await post(`${baseUrl}/webauthn/login/verify`, result);
73+
console.log('#### session', session);
74+
return session; // e.g., {accessToken, refreshToken, user}
75+
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14953,6 +14953,11 @@ react-native-pager-view@6.7.1:
1495314953
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.7.1.tgz#60d52dedbcc92ee7037a13287ebeed5f74e49df7"
1495414954
integrity sha512-cBSr6xw4g5N7Kd3VGWcf+kmaH7iBWb0DXAf2bVo3bXkzBcBbTOmYSvc0LVLHhUPW8nEq5WjT9LCIYAzgF++EXw==
1495514955

14956+
react-native-passkey@3.1.0:
14957+
version "3.1.0"
14958+
resolved "https://registry.yarnpkg.com/react-native-passkey/-/react-native-passkey-3.1.0.tgz#26617ed0cc115c6037e2537d264955d852395168"
14959+
integrity sha512-qOJ0q7AqbUOGXDOObcfiOJo1BOnG+lIQMEgltgeqmyMC+2XDlr7YrOQZgLwZbXheGC7BXPpwMSev/gxkgbt3xw==
14960+
1495614961
react-native-permissions@5.4.0:
1495714962
version "5.4.0"
1495814963
resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-5.4.0.tgz#04427be8897625ba5af29c3f1dc81b7bec48fd12"

0 commit comments

Comments
 (0)