diff --git a/.gitignore b/.gitignore
index f0fb5338..7588c24f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,6 +73,10 @@ rust/.cargo/config.toml
android/*.hprof
android/app/src/main/jniLibs
+# Sentry
+android/sentry.properties
+ios/sentry.properties
+
# react-native-config codegen
ios/envfile
diff --git a/android/sentry.properties b/android/sentry.properties
deleted file mode 100644
index e7f3c5ce..00000000
--- a/android/sentry.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-defaults.url=https://sentry.io/
-defaults.org=psychedelic
-defaults.project=plug-mobile
-auth.token=4e8aa37713c74bd39ccea5ebb54f5c30953411da65014645b8b5f53208b64f4c
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index d83df11d..d9a80e15 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -312,6 +312,8 @@ PODS:
- react-native-config/App (= 1.4.6)
- react-native-config/App (1.4.6):
- React-Core
+ - react-native-document-picker (8.1.1):
+ - React-Core
- react-native-file-access (2.5.0):
- React-Core
- ZIPFoundation (= 0.9.11)
@@ -526,6 +528,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-config (from `../node_modules/react-native-config`)
+ - react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-file-access (from `../node_modules/react-native-file-access`)
- react-native-minimizer (from `../node_modules/react-native-minimizer`)
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
@@ -638,6 +641,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/blur"
react-native-config:
:path: "../node_modules/react-native-config"
+ react-native-document-picker:
+ :path: "../node_modules/react-native-document-picker"
react-native-file-access:
:path: "../node_modules/react-native-file-access"
react-native-minimizer:
@@ -761,6 +766,7 @@ SPEC CHECKSUMS:
React-logger: ebb4d31bbbe4f1a8a1a9b658d7429210b8f68160
react-native-blur: 3e9c8e8e9f7d17fa1b94e1a0ae9fd816675f5382
react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e
+ react-native-document-picker: f68191637788994baed5f57d12994aa32cf8bf88
react-native-file-access: 204693d308701ba232dcf91facef05498fcde9a2
react-native-minimizer: b94809a769ac3825b46fd081d4f0ae2560791536
react-native-pager-view: 7f00d63688f7df9fad86dfb0154814419cc5eb8d
diff --git a/ios/sentry.properties b/ios/sentry.properties
deleted file mode 100644
index e7f3c5ce..00000000
--- a/ios/sentry.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-defaults.url=https://sentry.io/
-defaults.org=psychedelic
-defaults.project=plug-mobile
-auth.token=4e8aa37713c74bd39ccea5ebb54f5c30953411da65014645b8b5f53208b64f4c
diff --git a/package.json b/package.json
index 65728972..8ceb7ec2 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"@dfinity/principal": "^0.9.3",
"@hookform/error-message": "^2.0.0",
"@psychedelic/dab-js": "1.4.12",
- "@psychedelic/plug-controller": "0.23.2",
+ "@psychedelic/plug-controller": "0.23.4",
"@react-native-async-storage/async-storage": "^1.17.10",
"@react-native-community/blur": "^4.2.0",
"@react-native-community/clipboard": "^1.5.1",
@@ -77,6 +77,7 @@
"react-native-crypto": "^2.2.0",
"react-native-crypto-js": "^1.0.0",
"react-native-device-info": "^10.2.0",
+ "react-native-document-picker": "^8.1.1",
"react-native-fast-image": "^8.6.1",
"react-native-fetch-api": "^2.0.0",
"react-native-file-access": "^2.5.0",
diff --git a/src/components/common/GradientText/index.js b/src/components/common/GradientText/index.tsx
similarity index 54%
rename from src/components/common/GradientText/index.js
rename to src/components/common/GradientText/index.tsx
index 1fd8a31b..4fdf4c43 100644
--- a/src/components/common/GradientText/index.js
+++ b/src/components/common/GradientText/index.tsx
@@ -2,19 +2,30 @@ import MaskedView from '@react-native-masked-view/masked-view';
import React from 'react';
import LinearGradient from 'react-native-linear-gradient';
-import Text from '../Text';
+import Text, { TextProps } from '../Text';
-const GradientText = ({ colors, style, ...props }) => {
+interface Coordinate {
+ x: number;
+ y: number;
+}
+
+interface Props extends TextProps {
+ colors: string[];
+ start?: Coordinate;
+ end?: Coordinate;
+}
+
+function GradientText({ colors, style, start, end, ...props }: Props) {
return (
}>
+ start={start || { x: 0, y: 0 }}
+ end={end || { x: 1, y: 1 }}>
);
-};
+}
export default GradientText;
diff --git a/src/components/common/Text/index.tsx b/src/components/common/Text/index.tsx
index e4ae2357..2e677f4d 100644
--- a/src/components/common/Text/index.tsx
+++ b/src/components/common/Text/index.tsx
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
-import { Text as RNText, TextProps } from 'react-native';
+import { Text as RNText, TextProps as RNTextProps } from 'react-native';
export type TextTypes =
| 'headline1'
@@ -14,13 +14,13 @@ export type TextTypes =
| 'overline'
| 'normal';
-interface Props extends TextProps {
+export interface TextProps extends RNTextProps {
type?: TextTypes;
}
import styles from './styles';
-function Text({ style, type, ...props }: Props) {
+function Text({ style, type, ...props }: TextProps) {
const baseStyle = useMemo(
() => (type ? [styles.base, styles[type], style] : [styles.base, style]),
[type, style]
diff --git a/src/components/common/Text/styles.ts b/src/components/common/Text/styles.ts
index a82dbcf8..a02722e5 100644
--- a/src/components/common/Text/styles.ts
+++ b/src/components/common/Text/styles.ts
@@ -15,4 +15,5 @@ export default StyleSheet.create({
caption: FontStyles.Caption,
overline: FontStyles.Overline,
normal: FontStyles.Normal,
+ error: FontStyles.Error,
});
diff --git a/src/redux/slices/keyring.ts b/src/redux/slices/keyring.ts
index b900dc65..2ab4bf71 100644
--- a/src/redux/slices/keyring.ts
+++ b/src/redux/slices/keyring.ts
@@ -1,5 +1,5 @@
import { CreatePrincipalOptions } from '@psychedelic/plug-controller/dist/PlugKeyRing/interfaces';
-import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
+import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { KeyringState, State, Wallet } from '@/interfaces/redux';
import KeyRing from '@/modules/keyring';
@@ -200,6 +200,58 @@ export const login = createAsyncThunk(
}
);
+export const importAccountFromPem = createAsyncThunk(
+ 'keyring/importAccountFromPem',
+ async (
+ { pem, icon, name }: { name: string; icon: string; pem: string },
+ { rejectWithValue }
+ ) => {
+ try {
+ const instance = KeyRing.getInstance();
+ const response = await instance?.importAccountFromPem({
+ pem,
+ icon,
+ name,
+ });
+ return response;
+ } catch (e: any) {
+ // TODO: Add toast to handle error.
+ return rejectWithValue(e.message);
+ }
+ }
+);
+
+export const validatePem = createAsyncThunk(
+ 'keyring/validatePem',
+ async (
+ {
+ pem,
+ onSuccess,
+ onFailure,
+ onFinish,
+ }: {
+ pem: string;
+ onSuccess: () => void;
+ onFailure: () => void;
+ onFinish: () => void;
+ },
+ { rejectWithValue }
+ ) => {
+ try {
+ const instance = KeyRing.getInstance();
+ const response = await instance?.validatePem({ pem });
+ if (response) {
+ onSuccess();
+ } else {
+ onFailure();
+ }
+ onFinish();
+ } catch (e: any) {
+ return rejectWithValue({ error: e.message });
+ }
+ }
+);
+
export const createSubaccount = createAsyncThunk(
'keyring/createSubaccount',
async (params: CreatePrincipalOptions, { rejectWithValue }) => {
@@ -356,11 +408,6 @@ export const keyringSlice = createSlice({
state.isInitialized = action.payload.isInitialized;
state.isUnlocked = action.payload.isUnlocked;
})
- .addCase(createSubaccount.fulfilled, (state, action) => {
- if (action.payload) {
- state.wallets = [...state.wallets, action.payload];
- }
- })
.addCase(editSubaccount.fulfilled, (state, action) => {
const { isCurrentWallet, wallet } = action.payload;
if (isCurrentWallet) {
@@ -397,7 +444,15 @@ export const keyringSlice = createSlice({
state.isInitialized = true;
state.isUnlocked = unlocked;
}
- });
+ })
+ .addMatcher(
+ isAnyOf(createSubaccount.fulfilled, importAccountFromPem.fulfilled),
+ (state, action) => {
+ if (action.payload) {
+ state.wallets = [...state.wallets, action.payload];
+ }
+ }
+ );
},
});
diff --git a/src/screens/auth/ImportSeedPhrase/index.js b/src/screens/auth/ImportSeedPhrase/index.js
index 19da3f66..4450f928 100644
--- a/src/screens/auth/ImportSeedPhrase/index.js
+++ b/src/screens/auth/ImportSeedPhrase/index.js
@@ -88,7 +88,7 @@ const ImportSeedPhrase = ({ navigation, route }) => {
/>
- {t('importSeedPhrase.title')}
+ {t('common.importWallet')}
{t('importSeedPhrase.enterPhrase')}
diff --git a/src/screens/auth/Welcome/index.js b/src/screens/auth/Welcome/index.js
index f9740bb8..748ecad5 100644
--- a/src/screens/auth/Welcome/index.js
+++ b/src/screens/auth/Welcome/index.js
@@ -31,7 +31,7 @@ function Welcome() {
const title = isInitialized ? t('welcome.initTitle') : t('welcome.title');
const importTitle = isInitialized
? t('welcome.importNew')
- : t('welcome.import');
+ : t('common.importWallet');
const createTitle = isInitialized
? t('welcome.createNew')
: t('welcome.create');
diff --git a/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/index.tsx b/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/index.tsx
index 9e47b257..5f2a3a31 100644
--- a/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/index.tsx
+++ b/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/index.tsx
@@ -47,7 +47,6 @@ function BatchTransactions({ request, metadata }: WallectConnectFlowsData) {
{transactions.map(renderTransaction)}
diff --git a/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/styles.ts b/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/styles.ts
index d4588c03..2f84aacc 100644
--- a/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/styles.ts
+++ b/src/screens/flows/WalletConnect/screens/Flows/components/BatchTransactions/styles.ts
@@ -5,10 +5,10 @@ import { Colors } from '@/constants/theme';
export default StyleSheet.create({
listContentContainer: {
paddingVertical: 10,
- flexGrow: 1,
},
list: {
width: '100%',
+ flex: 1,
},
appIcon: {
height: 28,
diff --git a/src/screens/tabs/Profile/screens/Accounts/index.js b/src/screens/tabs/Profile/screens/Accounts/index.js
index e27aa760..b5669906 100644
--- a/src/screens/tabs/Profile/screens/Accounts/index.js
+++ b/src/screens/tabs/Profile/screens/Accounts/index.js
@@ -22,6 +22,7 @@ import { setCurrentPrincipal } from '@/redux/slices/keyring';
import shortAddress from '@/utils/shortAddress';
import CreateEditAccount from '../CreateEditAccount';
+import CreateImportAccount from '../CreateImportAccount';
import AddICNS from './AddICNS';
import styles from './styles';
@@ -39,15 +40,16 @@ const Accounts = ({ modalRef, onClose, ...props }) => {
const [selectedAccount, setSelectedAccount] = useState(null);
const createEditAccountRef = useRef(null);
+ const createImportAccountRef = useRef(null);
const addICNSRef = useRef(null);
useEffect(() => {
dispatch(getICPPrice());
}, []);
- const onCreateAccount = () => {
+ const onCreateImportAccount = () => {
setSelectedAccount(null);
- createEditAccountRef.current?.open();
+ createImportAccountRef.current?.open();
};
const onEditAccount = account => {
@@ -155,11 +157,11 @@ const Accounts = ({ modalRef, onClose, ...props }) => {
)}
{wallets?.map(renderAccountItem)}
-
+
- {t('accounts.createAccount')}
+ {t('accounts.createImportAccount')}
@@ -168,6 +170,10 @@ const Accounts = ({ modalRef, onClose, ...props }) => {
accountsModalRef={modalRef}
account={selectedAccount}
/>
+
{
+interface Props {
+ modalRef: RefObject;
+ accountsModalRef?: RefObject;
+ account?: Wallet;
+ pem?: string;
+ createImportModalRef?: RefObject;
+}
+
+const CreateEditAccount = ({
+ modalRef,
+ account,
+ accountsModalRef,
+ pem,
+ createImportModalRef,
+}: Props) => {
const { t } = useTranslation();
- const editEmojiRef = useRef(null);
+ const editEmojiRef = useRef(null);
const [accountName, setAccountName] = useState('');
const [editTouched, setEditTouched] = useState(false);
const [emoji, setEmoji] = useState('');
+ const dispatch = useAppDispatch();
- const { onCreate, onEdit } = useAccounts();
+ const title = account ? t('accounts.edit') : t('accounts.create');
+ const nameAndIcon = {
+ name: accountName,
+ icon: emoji,
+ };
const onPress = () => {
account
- ? onEdit({
- walletId: account.walletId,
- name: accountName,
- icon: emoji,
- })
- : onCreate({
- name: accountName,
- icon: emoji,
- });
+ ? dispatch(
+ editSubaccount({
+ walletId: account.walletId,
+ ...nameAndIcon,
+ })
+ )
+ : pem
+ ? dispatch(
+ importAccountFromPem({
+ ...nameAndIcon,
+ pem,
+ })
+ )
+ : dispatch(createSubaccount(nameAndIcon));
resetState();
+ if (createImportModalRef) {
+ createImportModalRef.current?.close();
+ }
modalRef.current?.close();
};
@@ -46,35 +81,25 @@ const CreateEditAccount = ({ modalRef, account, accountsModalRef }) => {
const onEditEmoji = () => {
Keyboard.dismiss();
- editEmojiRef?.current.open();
+ editEmojiRef?.current?.open();
setEditTouched(true);
};
- const getName = useCallback(
- isSave =>
- isSave
- ? t('accounts.save')
- : account
- ? t('accounts.edit')
- : t('accounts.create'),
- [account]
- );
-
useEffect(() => {
if (account) {
setAccountName(account.name);
- setEmoji(account.icon);
+ setEmoji(account.icon!);
} else {
resetState();
}
}, [account]);
const closeModal = () => {
- accountsModalRef?.current.close();
+ accountsModalRef?.current?.close();
};
const handleBack = () => {
- modalRef?.current.close();
+ modalRef?.current?.close();
};
const handleClose = () => {
@@ -86,17 +111,9 @@ const CreateEditAccount = ({ modalRef, account, accountsModalRef }) => {
return (
- {t('common.close')}
-
- }
- left={
-
- {t('common.back')}
-
- }
- center={{getName()}}
+ right={}
+ left={}
+ center={{title}}
/>
@@ -122,11 +139,7 @@ const CreateEditAccount = ({ modalRef, account, accountsModalRef }) => {
style={styles.input}
onChangeText={setAccountName}
/>
-
+
diff --git a/src/screens/tabs/Profile/screens/CreateEditAccount/styles.js b/src/screens/tabs/Profile/screens/CreateEditAccount/styles.ts
similarity index 92%
rename from src/screens/tabs/Profile/screens/CreateEditAccount/styles.js
rename to src/screens/tabs/Profile/screens/CreateEditAccount/styles.ts
index de83dee6..9aebfbe7 100644
--- a/src/screens/tabs/Profile/screens/CreateEditAccount/styles.js
+++ b/src/screens/tabs/Profile/screens/CreateEditAccount/styles.ts
@@ -29,7 +29,4 @@ export default StyleSheet.create({
toolTipText: {
color: Colors.Black.Pure,
},
- valid: {
- color: Colors.ActionBlue,
- },
});
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon.png
new file mode 100644
index 00000000..bff24ff0
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon@2x.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon@2x.png
new file mode 100644
index 00000000..98078aaa
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon@2x.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon@3x.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon@3x.png
new file mode 100644
index 00000000..3186590a
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/createIcon@3x.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon.png
new file mode 100644
index 00000000..a8005529
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon@2x.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon@2x.png
new file mode 100644
index 00000000..e0b97ea7
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon@2x.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon@3x.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon@3x.png
new file mode 100644
index 00000000..546a2d98
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/pemFileIcon@3x.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon.png
new file mode 100644
index 00000000..02ec1354
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon@2x.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon@2x.png
new file mode 100644
index 00000000..6926a467
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon@2x.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon@3x.png b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon@3x.png
new file mode 100644
index 00000000..d0cec186
Binary files /dev/null and b/src/screens/tabs/Profile/screens/CreateImportAccount/assets/privateKeyIcon@3x.png differ
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/index.tsx b/src/screens/tabs/Profile/screens/CreateImportAccount/index.tsx
new file mode 100644
index 00000000..f7d64227
--- /dev/null
+++ b/src/screens/tabs/Profile/screens/CreateImportAccount/index.tsx
@@ -0,0 +1,104 @@
+import { t } from 'i18next';
+import React, { RefObject, useRef } from 'react';
+import { Image, TouchableOpacity, View } from 'react-native';
+import DocumentPicker from 'react-native-document-picker';
+import { FileSystem } from 'react-native-file-access';
+import { Modalize } from 'react-native-modalize';
+
+import {
+ ActionButton,
+ GradientText,
+ Header,
+ Modal,
+ Text,
+} from '@/components/common';
+import { isIos } from '@/constants/platform';
+import { useStateWithCallback } from '@/hooks/useStateWithCallback';
+
+import CreateAccount from '../CreateEditAccount';
+import ImportKey from '../ImportKey';
+import styles from './styles';
+import { Button, getCreateImportButtons } from './utils';
+
+interface Props {
+ modalRef: RefObject;
+ accountsModalRef: RefObject;
+}
+
+function CreateImportAccount({ accountsModalRef, modalRef }: Props) {
+ const [pemFile, setPemFile] = useStateWithCallback('');
+ const createAccountRef = useRef(null);
+ const importKeyRef = useRef(null);
+
+ const closeModal = () => {
+ accountsModalRef?.current?.close();
+ };
+
+ const handleBack = () => {
+ modalRef?.current?.close();
+ };
+
+ const openCreateAccountModal = () => {
+ createAccountRef?.current?.open();
+ };
+
+ const openImportKeyModal = () => {
+ importKeyRef?.current?.open();
+ };
+
+ const openFile = async () => {
+ try {
+ const type = isIos
+ ? 'public.x509-certificate'
+ : ['.pem', 'application/x-pem-file'];
+ const res = await DocumentPicker.pickSingle({ type });
+
+ const stringifyPEM = await FileSystem.readFile(res.uri);
+ setPemFile(stringifyPEM, openCreateAccountModal);
+ } catch (e) {
+ // TODO: Add toast to handle this error.
+ console.log('Error opening .pem');
+ }
+ };
+
+ const renderButton = ({ id, title, onPress, icon, colors }: Button) => (
+
+
+
+ {title}
+
+
+ );
+
+ const buttons = getCreateImportButtons({
+ openCreateAccountModal,
+ openFile,
+ openImportKeyModal,
+ });
+
+ return (
+
+ }
+ left={}
+ center={
+ {t('accounts.createImportAccount')}
+ }
+ />
+ {buttons.map(renderButton)}
+
+
+
+ );
+}
+
+export default CreateImportAccount;
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/styles.ts b/src/screens/tabs/Profile/screens/CreateImportAccount/styles.ts
new file mode 100644
index 00000000..7eb307a9
--- /dev/null
+++ b/src/screens/tabs/Profile/screens/CreateImportAccount/styles.ts
@@ -0,0 +1,14 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingTop: 10,
+ paddingBottom: 30,
+ flexDirection: 'row',
+ justifyContent: 'space-evenly',
+ },
+ button: {
+ alignItems: 'center',
+ },
+});
diff --git a/src/screens/tabs/Profile/screens/CreateImportAccount/utils.ts b/src/screens/tabs/Profile/screens/CreateImportAccount/utils.ts
new file mode 100644
index 00000000..491bca99
--- /dev/null
+++ b/src/screens/tabs/Profile/screens/CreateImportAccount/utils.ts
@@ -0,0 +1,47 @@
+import { t } from 'i18next';
+import { ImageSourcePropType } from 'react-native';
+
+import createAccountIcon from './assets/createIcon.png';
+import pemFileIcon from './assets/pemFileIcon.png';
+import privateKeyIcon from './assets/privateKeyIcon.png';
+
+export interface Button {
+ id: string;
+ title: string;
+ onPress: () => void;
+ icon: ImageSourcePropType;
+ colors: string[];
+}
+
+export const getCreateImportButtons = ({
+ openCreateAccountModal,
+ openFile,
+ openImportKeyModal,
+}: {
+ openCreateAccountModal: () => void;
+ openFile: () => void;
+ openImportKeyModal: () => void;
+}) =>
+ [
+ {
+ id: 'create',
+ title: t('createImportAccount.create'),
+ onPress: openCreateAccountModal,
+ icon: createAccountIcon,
+ colors: ['#00E8FF', '#44DC45'],
+ },
+ {
+ id: 'importKey',
+ title: t('createImportAccount.importKey'),
+ onPress: openImportKeyModal,
+ icon: privateKeyIcon,
+ colors: ['#FB5DC3', '#FDB943'],
+ },
+ {
+ id: 'importPem',
+ title: t('createImportAccount.importPem'),
+ onPress: openFile,
+ icon: pemFileIcon,
+ colors: ['#36C3E9', '#CF6ED3'],
+ },
+ ] as Button[];
diff --git a/src/screens/tabs/Profile/screens/ImportKey/index.tsx b/src/screens/tabs/Profile/screens/ImportKey/index.tsx
new file mode 100644
index 00000000..ed3255e5
--- /dev/null
+++ b/src/screens/tabs/Profile/screens/ImportKey/index.tsx
@@ -0,0 +1,100 @@
+import { t } from 'i18next';
+import React, { RefObject, useRef, useState } from 'react';
+import { View } from 'react-native';
+import { Modalize } from 'react-native-modalize';
+
+import RainbowButton from '@/components/buttons/RainbowButton';
+import {
+ ActionButton,
+ Header,
+ Modal,
+ Text,
+ TextInput,
+} from '@/components/common';
+import { useAppDispatch } from '@/redux/hooks';
+import { validatePem } from '@/redux/slices/keyring';
+
+import CreateEditAccount from '../CreateEditAccount';
+import styles from './styles';
+
+interface Props {
+ modalRef: RefObject;
+ createImportRef: RefObject;
+ accountsModalRef: RefObject;
+}
+
+function ImportKey({ createImportRef, modalRef, accountsModalRef }: Props) {
+ const createEditAccount = useRef(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(false);
+ const [key, setKey] = useState('');
+ const dispatch = useAppDispatch();
+ const disabled = key === '' || loading || error;
+
+ const handleOnChangeKey = (value: string) => {
+ if (error) {
+ setError(false);
+ }
+ setKey(value);
+ };
+
+ const closeModal = () => {
+ createImportRef?.current?.close();
+ accountsModalRef?.current?.close();
+ };
+
+ const handleBack = () => {
+ modalRef?.current?.close();
+ };
+
+ const handleContinue = () => {
+ setLoading(true);
+ dispatch(
+ validatePem({
+ pem: key,
+ onSuccess: () => createEditAccount.current?.open(),
+ onFailure: () => setError(true),
+ onFinish: () => setLoading(false),
+ })
+ );
+ };
+
+ return (
+
+ }
+ left={}
+ center={{t('common.importWallet')}}
+ />
+
+
+ {error && (
+
+ {t('createImportAccount.invalidKey')}
+
+ )}
+
+
+
+
+ );
+}
+
+export default ImportKey;
diff --git a/src/screens/tabs/Profile/screens/ImportKey/styles.ts b/src/screens/tabs/Profile/screens/ImportKey/styles.ts
new file mode 100644
index 00000000..9972aef0
--- /dev/null
+++ b/src/screens/tabs/Profile/screens/ImportKey/styles.ts
@@ -0,0 +1,21 @@
+import { StyleSheet } from 'react-native';
+
+import { Colors } from '@/constants/theme';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingHorizontal: 24,
+ paddingVertical: 32,
+ },
+ inputStyle: {
+ width: '100%',
+ marginBottom: 8,
+ },
+ button: {
+ marginTop: 16,
+ },
+ error: {
+ color: Colors.Red,
+ },
+});
diff --git a/src/translations/en/index.js b/src/translations/en/index.js
index 12e1b31e..5b195096 100644
--- a/src/translations/en/index.js
+++ b/src/translations/en/index.js
@@ -30,13 +30,13 @@ const translations = {
questionMark: '?',
the: 'The',
enterPassword: 'Enter Password',
+ importWallet: 'Import Wallet',
},
welcome: {
title: 'Welcome to Plug',
initTitle: 'Choose an option',
create: 'Create Wallet',
createNew: 'Create New Wallet',
- import: 'Import Wallet',
importNew: 'Import New Wallet',
},
login: {
@@ -46,7 +46,6 @@ const translations = {
moreOptions: 'More options',
},
importSeedPhrase: {
- title: 'Import Wallet',
enterPhrase: 'Please enter your 12 word Secret Recovery Phrase.',
secretPhrase: 'Secret Recovery Phrase',
notFound: 'Recovery phrase not found',
@@ -167,7 +166,7 @@ const translations = {
},
accounts: {
title: 'Accounts',
- createAccount: 'Create account',
+ createImportAccount: 'Create/Import Account',
moreOptions: {
edit: 'Edit Account',
addIcns: 'Add ICNS Domain',
@@ -188,7 +187,6 @@ const translations = {
editEmoji: 'Edit emoji',
editButton: '👈 Edit',
accountNamePlaceholder: 'Account name',
- save: 'Save Account',
edit: 'Edit Account',
create: 'Create Account',
},
@@ -306,6 +304,12 @@ const translations = {
'You can always recover your accounts through your Secret recovery Phrase given that your accounts exists on the blockchain.',
action: 'Delete Wallet',
},
+ createImportAccount: {
+ importKey: 'Private Key',
+ importPem: 'PEM File',
+ create: 'Create',
+ invalidKey: 'Invalid key. Please, try again.',
+ },
};
export default translations;
diff --git a/yarn.lock b/yarn.lock
index 98337785..b8783d1e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1777,10 +1777,10 @@
cross-fetch "^3.1.4"
crypto-js "^4.1.1"
-"@psychedelic/plug-controller@0.23.2":
- version "0.23.2"
- resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.23.2/1a7c8dccd0edc69d448c37317caf7e2c04f5a3bb#1a7c8dccd0edc69d448c37317caf7e2c04f5a3bb"
- integrity sha512-sfShtvoIdotShcP9tOfGuB9a7XEs7qmRlQ8EbAUKdIs7wFQyLNqXOkqxCyUjbD4EsC5dtFiMo5dpZ0xGfv71Sg==
+"@psychedelic/plug-controller@0.23.4":
+ version "0.23.4"
+ resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.23.4/92ba5923065d37ea322cfc758861da0694959cfa#92ba5923065d37ea322cfc758861da0694959cfa"
+ integrity sha512-fwdlVPZidcHFt6x+o5OXKSyJi/VA7htkpZJ6UcU6WromEsBenaHKuRPdfJgJzRpY5AMWckgLKf2HVPL1fHxxJg==
dependencies:
"@dfinity/agent" "0.9.3"
"@dfinity/candid" "0.9.3"
@@ -8867,6 +8867,13 @@ react-native-device-info@^10.2.0:
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-10.2.0.tgz#be17a1b86cabbe53f4adde96b662204784d3cefd"
integrity sha512-7VycjFGaLKIZLDwCxSK5cWZdDOGqz3QxdGF57B/a4bW3gkrUFa5Gf0v7fUR9YL/VBJM0UwDiDH7pR9hkMQt+vw==
+react-native-document-picker@^8.1.1:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/react-native-document-picker/-/react-native-document-picker-8.1.1.tgz#642bbe25752cc428b96416318f8dc07cef29ee10"
+ integrity sha512-mH0oghd7ndgU9/1meVJdqts1sAkOfUQW1qbrqTTsvR5f2K9r0BAj/X02dve5IBMOMZvlGd7qWrNVuIFg5AUXWg==
+ dependencies:
+ invariant "^2.2.4"
+
react-native-fast-image@^8.6.1:
version "8.6.1"
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.1.tgz#6a3a11b8ebc7629919265d33a420a04d13c897e0"