Skip to content

Commit

Permalink
add/connect wallet flow (#4239)
Browse files Browse the repository at this point in the history
* add/connect wallet

* advanced import wallet

* mnemonic word adjustments

* username input adjustments

* updates

* cleanup

* fix

* turn off pssword when biometrics eanbled

* undo images
  • Loading branch information
peterpme authored Jun 27, 2023
1 parent f8bb13d commit 72bb031
Show file tree
Hide file tree
Showing 13 changed files with 657 additions and 387 deletions.
6 changes: 6 additions & 0 deletions packages/app-mobile/src/Images.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
const Images = {
solanaLogo: require("../assets/blockchains/solana.png"),
ethereumLogo: require("../assets/blockchains/ethereum.png"),
logoAvalanche: require("./images/logo-avalanche.png"),
logoBsc: require("./images/logo-bsc.png"),
logoCosmos: require("./images/logo-cosmos.png"),
logoEthereum: require("./images/logo-ethereum.png"),
logoPolygon: require("./images/logo-polygon.png"),
logoSolana: require("./images/logo-solana.png"),
};

export default Images;
131 changes: 131 additions & 0 deletions packages/app-mobile/src/components/MnemonicInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { useCallback, useEffect, useState } from "react";
import { Alert, Keyboard, Pressable } from "react-native";

import {
UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_CREATE,
UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC,
} from "@coral-xyz/common";
import { useBackgroundClient } from "@coral-xyz/recoil";
import { StyledText, YStack } from "@coral-xyz/tamagui";

import { maybeRender } from "~lib/index";

import { MnemonicInputFields } from "~src/components/MnemonicInputFields";
import { CopyButton, PasteButton } from "~src/components/index";

type MnemonicInputProps = {
readOnly: boolean;
onComplete: ({
mnemonic,
isValid,
}: {
mnemonic: string;
isValid: boolean;
}) => void;
};
export function MnemonicInput({ readOnly, onComplete }: MnemonicInputProps) {
const background = useBackgroundClient();
const [keyboardStatus, setKeyboardStatus] = useState("");

const [mnemonicWords, setMnemonicWords] = useState<string[]>([
...Array(12).fill(""),
]);

const mnemonic = mnemonicWords.map((f) => f.trim()).join(" ");

useEffect(() => {
const showSubscription = Keyboard.addListener("keyboardDidShow", () => {
setKeyboardStatus("shown");
});
const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
setKeyboardStatus("hidden");
});

return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);

const generateRandom = useCallback(() => {
background
.request({
method: UI_RPC_METHOD_KEYRING_STORE_MNEMONIC_CREATE,
params: [mnemonicWords.length === 12 ? 128 : 256],
})
.then((m: string) => {
const words = m.split(" ");
setMnemonicWords(words);
});
}, []); // eslint-disable-line

useEffect(() => {
if (readOnly) {
generateRandom();
}
}, [readOnly, generateRandom]);

const isValidAsync = (mnemonic: string) => {
return background.request({
method: UI_RPC_METHOD_KEYRING_VALIDATE_MNEMONIC,
params: [mnemonic],
});
};

const onChange = async (words: string[]) => {
setMnemonicWords(words);
if (readOnly) {
const mnemonic = mnemonicWords.map((f) => f.trim()).join(" ");
onComplete({ isValid: true, mnemonic });
return;
}

if (words.length > 11) {
const mnemonic = mnemonicWords.map((f) => f.trim()).join(" ");
const isValid = words.length > 11 ? await isValidAsync(mnemonic) : false;
onComplete({ isValid, mnemonic });
}
};

return (
<YStack space={8}>
<MnemonicInputFields
mnemonicWords={mnemonicWords}
onChange={readOnly ? undefined : onChange}
onComplete={async () => {
const isValid = await isValidAsync(mnemonic);
onComplete({ isValid, mnemonic });
}}
/>
{readOnly ? (
<CopyButton text={mnemonicWords.join(", ")} />
) : keyboardStatus === "shown" ? null : (
<PasteButton
onPaste={(words) => {
const split = words.split(" ");
if ([12, 24].includes(split.length)) {
setMnemonicWords(words.split(" "));
} else {
Alert.alert("Mnemonic should be either 12 or 24 words");
}
}}
/>
)}
{maybeRender(!readOnly, () => (
<Pressable
hitSlop={12}
onPress={() => {
setMnemonicWords([
...Array(mnemonicWords.length === 12 ? 24 : 12).fill(""),
]);
}}
>
<StyledText fontSize="$sm" textAlign="center">
Use a {mnemonicWords.length === 12 ? "24" : "12"}-word recovery
mnemonic
</StyledText>
</Pressable>
))}
</YStack>
);
}
23 changes: 19 additions & 4 deletions packages/app-mobile/src/components/MnemonicInputFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@ type MnemonicWordInputProps = {
returnKeyType: "next" | "done";
onChangeText: (word: string) => void;
onSubmitEditing: () => void;
onBlur: () => void;
};

const _MnemonicWordInput = forwardRef<TextInput, MnemonicWordInputProps>(
(props, ref) => {
const { word, index, returnKeyType, onChangeText, onSubmitEditing } = props;
const {
word,
index,
returnKeyType,
onChangeText,
onSubmitEditing,
onBlur,
} = props;
const theme = useTheme();
return (
<View
Expand Down Expand Up @@ -48,6 +56,7 @@ const _MnemonicWordInput = forwardRef<TextInput, MnemonicWordInputProps>(
spellCheck={false}
scrollEnabled={false}
onSubmitEditing={onSubmitEditing}
onBlur={onBlur}
maxLength={10}
value={word}
style={[
Expand Down Expand Up @@ -103,7 +112,7 @@ export function MnemonicInputFields({
if (next) {
next.focus();
} else {
onComplete();
onComplete?.();
}
},
[onComplete]
Expand All @@ -123,6 +132,11 @@ export function MnemonicInputFields({
index={index}
returnKeyType={index === mnemonicWords.length - 1 ? "done" : "next"}
onSubmitEditing={selectNextInput(index)}
onBlur={() => {
if (mnemonicWords.length > 11) {
onComplete?.();
}
}}
onChangeText={(word) => {
if (onChange) {
const newMnemonicWords = [...mnemonicWords];
Expand All @@ -133,20 +147,21 @@ export function MnemonicInputFields({
/>
);
},
[mnemonicWords, onChange, selectNextInput]
[mnemonicWords, onChange, selectNextInput, onComplete]
);

return (
<FlatList
data={mnemonicWords}
numColumns={3}
initialNumToRender={12}
scrollEnabled={false}
scrollEnabled={mnemonicWords.length > 12}
keyExtractor={keyExtractor}
contentContainerStyle={{ gap: ITEM_GAP }}
columnWrapperStyle={{ gap: ITEM_GAP }}
renderItem={renderItem}
maxToRenderPerBatch={12}
style={{ maxHeight: 275 }}
getItemLayout={(_data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
Expand Down
93 changes: 54 additions & 39 deletions packages/app-mobile/src/components/StyledTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,49 +47,64 @@ function Container({
);
}

type UsernameInputProps = {
username: string;
onChange: (username: string) => void;
onComplete: () => void;
showError?: boolean;
disabled?: boolean;
};
type UsernameInputProps = TextInputProps &
UseControllerProps & {
username: string;
onChange: (username: string) => void;
onComplete: () => void;
showError?: boolean;
disabled?: boolean;
};

export function UsernameInput({
onSubmitEditing,
autoFocus,
control,
showError,
username,
onChange,
onComplete,
disabled,
}: UsernameInputProps): JSX.Element {
const [localError, setLocalError] = useState(false);
return (
<Container hasError={localError || showError}>
<XStack>
<StyledText color="$fontColor">@</StyledText>
<RNTextInput
style={{ paddingLeft: 4 }}
autoFocus
placeholder="Username"
autoCapitalize="none"
returnKeyType="done"
maxLength={15}
value={username}
onSubmitEditing={onComplete}
onChangeText={(text) => {
const username = text.toLowerCase().replace(/[^a-z0-9_]/g, "");
if (
username !== "" &&
(username.length < 4 || username.length > 15)
) {
setLocalError(true);
} else {
setLocalError(false);
}
onChange(username);
}}
/>
</XStack>
</Container>
<Controller
name="username"
control={control}
rules={{
required: true,
minLength: {
value: 3,
message: "Username must be at least 3 characters",
},
maxLength: {
value: 15,
message: "Username must be less than 15 characters",
},
}}
render={({
field: { onChange, onBlur, value },
fieldState: { invalid },
}) => (
<Container hasError={invalid || showError}>
<XStack ai="center" h={48}>
<StyledText color="$fontColor">@</StyledText>
<RNTextInput
style={{
height: 48,
flex: 1,
paddingLeft: 4,
}}
autoFocus={autoFocus}
autoCorrect={false}
placeholder="Username"
autoCapitalize="none"
returnKeyType="done"
maxLength={15}
value={value}
onChangeText={onChange}
onBlur={onBlur}
onSubmitEditing={onSubmitEditing}
/>
</XStack>
</Container>
)}
/>
);
}

Expand Down
Loading

1 comment on commit 72bb031

@vercel
Copy link

@vercel vercel bot commented on 72bb031 Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.