AccountSeed.tsx 5.71 KB
Newer Older
1
// Copyright 2015-2020 Parity Technologies (UK) Ltd.
Tomasz Drwięga's avatar
Tomasz Drwięga committed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// This file is part of Parity.

// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity.  If not, see <http://www.gnu.org/licenses/>.

17
import React, { ReactElement, useState } from 'react';
18
19
20
21
22
23
24
25
26
import {
	NativeSyntheticEvent,
	StyleSheet,
	Text,
	TextInputProps,
	TextInputSelectionChangeEventData,
	View
} from 'react-native';

27
28
import TextInput from './TextInput';
import TouchableItem from './TouchableItem';
29
30
31
32
33
34
35

import colors from 'styles/colors';
import fonts from 'styles/fonts';
import fontStyles from 'styles/fontStyles';
import PARITY_WORDS from 'res/parity_wordlist.json';
import BIP39_WORDS from 'res/bip39_wordlist.json';
import { binarySearch } from 'utils/array';
Thibaut Sardan's avatar
Thibaut Sardan committed
36
37
38
39

// Combined, de-duplicated, sorted word list (could be a precompute from json as well)
const ALL_WORDS = Array.from(new Set(PARITY_WORDS.concat(BIP39_WORDS))).sort();
const SUGGESTIONS_COUNT = 5;
Alexey's avatar
Alexey committed
40

41
42
43
44
45
interface Props extends TextInputProps {
	onChangeText: (text: string) => void;
	valid: boolean;
}

46
47
48
49
50
51
52
53
54
55
56
export default function AccountSeed({
	valid,
	onChangeText,
	...props
}: Props): React.ReactElement {
	const [cursorPosition, setCursorPosition] = useState({
		end: 0,
		start: 0
	});
	const [value, setValue] = useState('');
	function handleCursorPosition(
57
		event: NativeSyntheticEvent<TextInputSelectionChangeEventData>
58
59
60
	): void {
		setCursorPosition(event.nativeEvent.selection);
	}
61
62
63
64

	/**
	 * Generate a list of suggestions for input
	 */
65
	function generateSuggestions(input: string, wordList: string[]): string[] {
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
		const fromIndex = binarySearch(wordList, input).index; // index to start search from

		let suggestions = wordList.slice(fromIndex, fromIndex + SUGGESTIONS_COUNT);

		const lastValidIndex = suggestions.findIndex(
			word => !word.startsWith(input)
		);

		if (lastValidIndex !== -1) {
			suggestions = suggestions.slice(0, lastValidIndex);
		}

		return suggestions;
	}

81
	function selectWordList(otherWords: string[]): string[] {
82
83
84
85
86
87
88
89
90
91
92
		for (const word of otherWords) {
			const isBIP39 = binarySearch(BIP39_WORDS, word).hit;
			const isParity = binarySearch(PARITY_WORDS, word).hit;

			if (!isBIP39 && isParity) {
				return PARITY_WORDS;
			} else if (isBIP39 && !isParity) {
				return BIP39_WORDS;
			}
		}

93
		return ALL_WORDS as string[];
94
95
	}

96
97
98
99
	function onNativeChangeText(text: string): void {
		setValue(text);
		onChangeText(text);
	}
100

101
102
103
104
105
106
107
108
109
110
111
112
113
	function renderSuggestions(): ReactElement {
		const { start, end } = cursorPosition;
		if (start !== end) return <View style={styles.suggestions} />;
		const currentPosition = end;
		let left = value.substring(0, currentPosition).split(' ');
		let right = value.substring(currentPosition).split(' ');

		const isLeftSpace =
			currentPosition === 0 || value[currentPosition - 1] === '';
		const isRightSpace =
			currentPosition === value.length - 1 || value[currentPosition + 1] === '';
		const leftInput = isLeftSpace ? '' : left[left.length - 1];
		const rightInput = isRightSpace ? '' : right[0];
114
		// combine last nibble before cursor and first nibble after cursor into a word
115
		const input = leftInput + rightInput;
116
117
118
119
120

		left = left.slice(0, -1);
		right = right.slice(1);

		// find a wordList using words around as discriminator
121
122
		const wordList = selectWordList(left.concat(right));
		const suggestions = generateSuggestions(input.toLowerCase(), wordList);
123
124
125
126
127
128

		return (
			<View style={styles.suggestions}>
				{suggestions.map((suggestion, i) => {
					const sepStyle =
						i !== suggestions.length - 1
129
							? { borderColor: colors.border.light, borderRightWidth: 0.3 }
130
131
132
133
							: {};
					return (
						<TouchableItem
							key={i}
134
							onPress={(): void => {
135
								let phrase = left.concat(suggestion, right).join(' ').trimEnd();
136
137
								const is24words = phrase.split(' ').length === 24;
								if (!is24words) phrase += ' ';
138
								onNativeChangeText(phrase);
139
140
141
142
143
144
145
146
147
148
149
150
							}}
						>
							<View key={suggestion} style={[styles.suggestion, sepStyle]}>
								<Text style={styles.suggestionText}>{suggestion}</Text>
							</View>
						</TouchableItem>
					);
				})}
			</View>
		);
	}

151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
	const invalidStyles = !valid ? styles.invalidInput : {};
	return (
		<View>
			<TextInput
				style={StyleSheet.flatten([
					fontStyles.t_seed,
					styles.input,
					invalidStyles
				])}
				multiline
				autoCorrect={false}
				autoCompleteType="off"
				autoCapitalize="none"
				returnKeyType="done"
				blurOnSubmit={true}
				textAlignVertical="top"
				onSelectionChange={handleCursorPosition}
				selection={cursorPosition}
				value={value}
				onChangeText={onNativeChangeText}
				{...props}
			/>
			{value.length > 0 && renderSuggestions()}
		</View>
	);
Marek Kotewicz's avatar
Marek Kotewicz committed
176
177
178
}

const styles = StyleSheet.create({
179
180
181
182
	body: {
		flexDirection: 'column'
	},
	input: {
183
		borderBottomColor: colors.border.light
184
185
	},
	invalidInput: {
186
187
		borderBottomColor: colors.border.signal,
		borderColor: colors.border.signal
188
189
	},
	suggestion: {
190
		padding: 12,
Hanwen Cheng's avatar
Hanwen Cheng committed
191
		paddingVertical: 4
192
193
	},
	suggestionText: {
194
195
196
		color: colors.signal.main,
		fontFamily: fonts.regular,
		letterSpacing: fontStyles.t_seed.letterSpacing
197
198
199
	},
	suggestions: {
		alignItems: 'center',
200
201
		backgroundColor: colors.background.card,
		borderColor: colors.background.card,
Hanwen Cheng's avatar
Hanwen Cheng committed
202
		borderWidth: 0.5,
203
		flexDirection: 'row',
204
		height: 32,
Hanwen Cheng's avatar
Hanwen Cheng committed
205
206
207
		marginHorizontal: 16,
		marginTop: -8,
		overflow: 'hidden'
208
	}
Alexey's avatar
Alexey committed
209
});