diff --git a/.github/workflows/e2e-android.yml b/.github/workflows/e2e-android.yml index 5090b78fd..2696a40ed 100644 --- a/.github/workflows/e2e-android.yml +++ b/.github/workflows/e2e-android.yml @@ -62,6 +62,13 @@ jobs: with: node-version: 20 + - name: Configure npm authentication + run: | + echo "" >> .yarnrc.yml + echo "npmScopes:" >> .yarnrc.yml + echo " synonymdev:" >> .yarnrc.yml + echo ' npmAuthToken: "${{ secrets.NPMJS_READ_RN_PUBKY }}"' >> .yarnrc.yml + - name: Use gradle caches uses: actions/cache@v4 with: diff --git a/.github/workflows/e2e-ios.yml b/.github/workflows/e2e-ios.yml index bca72f89b..bd236826c 100644 --- a/.github/workflows/e2e-ios.yml +++ b/.github/workflows/e2e-ios.yml @@ -53,6 +53,13 @@ jobs: - name: Activate enviroment variables run: cp .env.test.template .env + - name: Configure npm authentication + run: | + echo "" >> .yarnrc.yml + echo "npmScopes:" >> .yarnrc.yml + echo " synonymdev:" >> .yarnrc.yml + echo ' npmAuthToken: "${{ secrets.NPMJS_READ_RN_PUBKY }}"' >> .yarnrc.yml + - name: Yarn Install run: yarn --no-audit --prefer-offline || yarn --no-audit --prefer-offline || yarn env: diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml index 1b5d55ff5..58c4b7701 100644 --- a/.github/workflows/jest.yml +++ b/.github/workflows/jest.yml @@ -30,7 +30,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - cache: 'yarn' + + - name: Configure npm authentication + run: | + echo "" >> .yarnrc.yml + echo "npmScopes:" >> .yarnrc.yml + echo " synonymdev:" >> .yarnrc.yml + echo ' npmAuthToken: "${{ secrets.NPMJS_READ_RN_PUBKY }}"' >> .yarnrc.yml - name: Install Node.js dependencies run: yarn install || yarn install diff --git a/.github/workflows/lint-check.yml b/.github/workflows/lint-check.yml index 2c1579b1b..fe68edf7b 100644 --- a/.github/workflows/lint-check.yml +++ b/.github/workflows/lint-check.yml @@ -19,7 +19,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - cache: 'yarn' + + - name: Configure npm authentication + run: | + echo "" >> .yarnrc.yml + echo "npmScopes:" >> .yarnrc.yml + echo " synonymdev:" >> .yarnrc.yml + echo ' npmAuthToken: "${{ secrets.NPMJS_READ_RN_PUBKY }}"' >> .yarnrc.yml - name: Install Node.js dependencies run: yarn install diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml index 7c37e1169..02748a548 100644 --- a/.github/workflows/type-check.yml +++ b/.github/workflows/type-check.yml @@ -19,7 +19,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - cache: 'yarn' + + - name: Configure npm authentication + run: | + echo "" >> .yarnrc.yml + echo "npmScopes:" >> .yarnrc.yml + echo " synonymdev:" >> .yarnrc.yml + echo ' npmAuthToken: "${{ secrets.NPMJS_READ_RN_PUBKY }}"' >> .yarnrc.yml - name: Install Node.js dependencies run: yarn install || yarn install diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b5780de08..6d0edc493 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1356,6 +1356,27 @@ PODS: - Yoga - react-native-netinfo (11.3.1): - React-Core + - react-native-pubky (0.3.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-quick-base64 (2.1.2): - DoubleConversion - glog @@ -1910,6 +1931,7 @@ DEPENDENCIES: - "react-native-ldk (from `../node_modules/@synonymdev/react-native-ldk`)" - react-native-mmkv (from `../node_modules/react-native-mmkv`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" + - "react-native-pubky (from `../node_modules/@synonymdev/react-native-pubky`)" - react-native-quick-base64 (from `../node_modules/react-native-quick-base64`) - react-native-quick-crypto (from `../node_modules/react-native-quick-crypto`) - react-native-restart (from `../node_modules/react-native-restart`) @@ -2061,6 +2083,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-mmkv" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" + react-native-pubky: + :path: "../node_modules/@synonymdev/react-native-pubky" react-native-quick-base64: :path: "../node_modules/react-native-quick-base64" react-native-quick-crypto: @@ -2213,6 +2237,7 @@ SPEC CHECKSUMS: react-native-ldk: 1d25080cfadac349eab355725da66de140fbc7a8 react-native-mmkv: 7d0b6c2a79e73100b933f2947a9c8741d664e18b react-native-netinfo: bdb108d340cdb41875c9ced535977cac6d2ff321 + react-native-pubky: 9fd2633ee974bafa9b77e0cd59e2619a0d9d708d react-native-quick-base64: f98f17faf04c9779faf726921a2b389d4775e8b6 react-native-quick-crypto: 12de8e1666ad3dab6339418c14f4a6de71716194 react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 @@ -2265,7 +2290,7 @@ SPEC CHECKSUMS: SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d sodium-react-native-direct: 8feb9a6d0d88ce65efa305d6cc774c11c62d9a15 SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef - Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 + Yoga: 2a45d7e59592db061217551fd3bbe2dd993817ae ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 PODFILE CHECKSUM: 8c2c3949d19327675be00d5f066e8eab99dd1e04 diff --git a/package.json b/package.json index d249e3d05..a578a24e9 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@synonymdev/react-native-keychain": "8.2.2", "@synonymdev/react-native-ldk": "0.0.152", "@synonymdev/react-native-lnurl": "0.0.10", + "@synonymdev/react-native-pubky": "^0.3.0", "@synonymdev/result": "0.0.2", "@synonymdev/slashtags-keychain": "1.0.0", "@synonymdev/slashtags-profile": "2.0.0", diff --git a/src/navigation/bottom-sheet/BottomSheets.tsx b/src/navigation/bottom-sheet/BottomSheets.tsx index 410fcc9c3..e9ec78a98 100644 --- a/src/navigation/bottom-sheet/BottomSheets.tsx +++ b/src/navigation/bottom-sheet/BottomSheets.tsx @@ -14,6 +14,7 @@ import PINNavigation from './PINNavigation'; import ReceiveNavigation from './ReceiveNavigation'; import SendNavigation from './SendNavigation'; import TreasureHuntNavigation from './TreasureHuntNavigation'; +import PubkyAuth from './PubkyAuth.tsx'; const BottomSheets = (): JSX.Element => { const views = useAppSelector(viewControllersSelector); @@ -31,6 +32,7 @@ const BottomSheets = (): JSX.Element => { {views.receiveNavigation.isMounted && } {views.sendNavigation.isMounted && } {views.treasureHunt.isMounted && } + {views.pubkyAuth.isMounted && } ); }; diff --git a/src/navigation/bottom-sheet/PubkyAuth.tsx b/src/navigation/bottom-sheet/PubkyAuth.tsx new file mode 100644 index 000000000..8fd3bc304 --- /dev/null +++ b/src/navigation/bottom-sheet/PubkyAuth.tsx @@ -0,0 +1,253 @@ +import React, { memo, ReactElement, useCallback, useEffect, useMemo } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; + +import { BodyM, CaptionB, Text13UP, Title } from '../../styles/text'; +import BottomSheetWrapper from '../../components/BottomSheetWrapper'; +import SafeAreaInset from '../../components/SafeAreaInset'; +import Button from '../../components/buttons/Button'; +import BottomSheetNavigationHeader from '../../components/BottomSheetNavigationHeader'; +import { useAppSelector } from '../../hooks/redux'; +import { + useSnapPoints, +} from '../../hooks/bottomSheet'; +import { viewControllerSelector } from '../../store/reselect/ui.ts'; +import { auth, parseAuthUrl } from '@synonymdev/react-native-pubky'; +import { getPubkySecretKey } from '../../utils/pubky'; +import { showToast } from '../../utils/notifications.ts'; +import { dispatch } from '../../store/helpers.ts'; +import { closeSheet } from '../../store/slices/ui.ts'; +import { CheckCircleIcon } from '../../styles/icons.ts'; +import Animated, { FadeIn } from 'react-native-reanimated'; + +const defaultParsedUrl: PubkyAuthDetails = { + relay: '', + capabilities: [{ + path: '', + permission: '', + }], + secret: '', +}; + +type Capability = { + path: string; + permission: string; +}; + +type PubkyAuthDetails = { + relay: string; + capabilities: Capability[]; + secret: string; +}; + +const Permission = memo(({ capability, authSuccess }: { capability: Capability; authSuccess: boolean }): ReactElement => { + return ( + + + {capability.path} + + + + {capability.permission.includes('r') && ( + Read + + )} + + + {capability.permission.includes('w') && ( + Write + )} + + + + ); +}); + +const PubkyAuth = (): ReactElement => { + const { t } = useTranslation('security'); + const snapPoints = useSnapPoints('medium'); + const { url = '' } = useAppSelector((state) => { + return viewControllerSelector(state, 'pubkyAuth'); + }); + const [parsed, setParsed] = React.useState(defaultParsedUrl); + const [authorizing, setAuthorizing] = React.useState(false); + const [authSuccess, setAuthSuccess] = React.useState(false); + + useEffect(() => { + const fetchParsed = async (): Promise => { + const res = await parseAuthUrl(url); + if (res.isErr()) { + console.log(res.error.message); + return; + } + setParsed(res.value); + }; + fetchParsed().then(); + + return (): void => { + setParsed(defaultParsedUrl); + setAuthorizing(false); + setAuthSuccess(false); + }; + }, [url]); + + const onAuthorize = useMemo(() => async (): Promise => { + try { + setAuthorizing(true); + const secretKey = await getPubkySecretKey(); + if (secretKey.isErr()) { + showToast({ + type: 'error', + title: t('authorization.pubky_secret_error_title'), + description: t('authorization.pubky_secret_error_description'), + }); + setAuthorizing(false); + return; + } + const authRes = await auth(url, secretKey.value); + if (authRes.isErr()) { + showToast({ + type: 'error', + title: t('authorization.pubky_auth_error_title'), + description: t('authorization.pubky_auth_error_description'), + }); + setAuthorizing(false); + return; + } + setAuthSuccess(true); + setAuthorizing(false); + } catch (e) { + showToast({ + type: 'error', + title: t('authorization.pubky_auth_error_title'), + description: JSON.stringify(e), + }); + setAuthorizing(false); + } + }, [t, url]); + + const onClose = useMemo(() => (): void => { + dispatch(closeSheet('pubkyAuth')); + }, []); + + const Buttons = useCallback(() => { + if (authSuccess) { + return ( +