)}
- {error && (
+
+ {indicateError && (
- {error}
+ {error || submitError}
)}
);
-});
+}
diff --git a/src/indigo-react/components/SelectInput.js b/src/indigo-react/components/SelectInput.js
index 064b7cfdb..0c8a5d4c8 100644
--- a/src/indigo-react/components/SelectInput.js
+++ b/src/indigo-react/components/SelectInput.js
@@ -1,69 +1,51 @@
import React, { useCallback, useRef, useState } from 'react';
import cn from 'classnames';
+import { useField } from 'react-final-form';
+import useOnClickOutside from 'indigo-react/lib/useOnClickOutside';
import Flex from './Flex';
import { ErrorText } from './Typography';
-import useOnClickOutside from 'indigo-react/lib/useOnClickOutside';
import AccessoryIcon from './AccessoryIcon';
// NOTE: if we really care about accessibility, we should pull in a dependency
export default function SelectInput({
- // visuals
name,
label,
+ placeholder,
className,
- accessory,
mono = false,
- placeholder,
-
- // callbacks
- onEnter,
-
- // state from hook
- focused,
- pass,
- syncPass,
- visiblyPassed,
- error,
- hintError,
- data,
- bind,
- autoFocus,
- disabled,
- options,
- touched,
+ options = [],
+ disabled = false,
+ warning,
+}) {
+ const {
+ input,
+ meta: { active, error, submitting, submitSucceeded, touched, valid },
+ } = useField(name, {
+ type: 'select',
+ });
- // ignored
- initialValue,
- validators,
- transformers,
+ disabled = disabled || submitting || submitSucceeded;
- // extra
- ...rest
-}) {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef();
// close select on outside clicks
useOnClickOutside(ref, useCallback(() => setIsOpen(false), [setIsOpen]));
- const toggleOpen = useCallback(() => setIsOpen(isOpen => !isOpen), [
- setIsOpen,
- ]);
+ const toggleOpen = useCallback(() => {
+ input.onFocus();
+ setIsOpen(isOpen => !isOpen);
+ }, [input]);
const onChange = value => {
- // TODO: provide setValue here?
// construct a pseudo event that sets the value correctly
- bind.onChange({ target: { value } });
+ input.onChange({ target: { value } });
+ input.onBlur();
setIsOpen(false);
};
- // redefine accessory because we still want to ignore it from the ..rest above
- accessory = (
-
{isOpen ? '▲' : '▼'}
- );
-
- const text = options.find(o => o.value === data).text;
+ const text = options.find(o => o.value === input.value).text;
return (
+ name={name}>
{isOpen ? placeholder : text}
- {accessory}
+
{isOpen ? '▲' : '▼'}
{isOpen && (
)}
- {error && (
+
+ {warning && (
+
+ {warning}
+
+ )}
+
+ {touched && !active && error && (
{error}
diff --git a/src/indigo-react/components/ToggleInput.js b/src/indigo-react/components/ToggleInput.js
index 6c5048c88..0f067719e 100644
--- a/src/indigo-react/components/ToggleInput.js
+++ b/src/indigo-react/components/ToggleInput.js
@@ -3,6 +3,7 @@ import cn from 'classnames';
import Flex from './Flex';
import LinkButton from './LinkButton';
+import { useField } from 'react-final-form';
export default function ToggleInput({
// visuals
@@ -11,32 +12,21 @@ export default function ToggleInput({
inverseLabel,
className,
- // callbacks
- onEnter,
-
- // state from hook
- focused,
- pass,
- syncPass,
- visiblyPassed,
- error,
- hintError,
- data,
- bind,
- autoFocus,
- disabled,
- touched,
-
- // ignored
- initialValue,
- validators,
- transformers,
+ //
+ disabled = false,
...rest
}) {
+ const {
+ input,
+ meta: { submitting, submitSucceeded },
+ } = useField(name, { type: 'checkbox' });
+
+ disabled = disabled || submitting || submitSucceeded;
+
return (
{/* and then display a prettier one in its stead */}
- {bind.checked ? inverseLabel : label}
+ {input.checked ? inverseLabel : label}
diff --git a/src/indigo-react/index.js b/src/indigo-react/index.js
index 20827cf09..b1cd906a2 100644
--- a/src/indigo-react/index.js
+++ b/src/indigo-react/index.js
@@ -11,5 +11,3 @@ export { default as IconButton } from './components/IconButton';
export { default as LinkButton } from './components/LinkButton';
export * from './components/Typography';
-
-export * from './lib';
diff --git a/src/indigo-react/lib/index.js b/src/indigo-react/lib/index.js
deleted file mode 100644
index 78ab1347a..000000000
--- a/src/indigo-react/lib/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as useForm } from './useForm';
diff --git a/src/indigo-react/lib/useForm.js b/src/indigo-react/lib/useForm.js
deleted file mode 100644
index 675a93005..000000000
--- a/src/indigo-react/lib/useForm.js
+++ /dev/null
@@ -1,264 +0,0 @@
-import { useCallback, useMemo } from 'react';
-import { keyBy, get, every, some } from 'lodash';
-
-import { compose } from 'lib/lib';
-import { kDefaultValidator } from 'lib/validators';
-import useSetState from 'lib/useSetState';
-import useDeepEqualReference from 'lib/useDeepEqualReference';
-
-// interface InputConfig {
-// name: string;
-// validators: Function[];
-// transformers: Function[];
-// initialValue: string;
-// autoFocus: boolean;
-// disabled: boolean;
-// ...extra,
-// }
-
-const defaultsFor = (configs, mapper) =>
- configs.reduce(
- (memo, config) => ({ ...memo, [config.name]: mapper(config) }),
- {}
- );
-
-/**
- * useForm manages a set of inputs for rendering them in a loop
- */
-export default function useForm(inputConfigs = []) {
- const configs = useDeepEqualReference(inputConfigs);
-
- const byName = useMemo(() => keyBy(configs, 'name'), [configs]);
-
- // track values
- const [values, _setValues, clearValues] = useSetState(() =>
- defaultsFor(configs, config =>
- config.initialValue === undefined ? '' : config.initialValue
- )
- );
- // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
- // configs that are added, we won't be able to set an initialValue for a
- // dynamic form that then triggers validation. for initial configs,
- // initialValue can be a validated string and everything will be happy,
- // but for dynamically added input configs, we won't get validation, etc
- // until there's a traditional state update.
-
- // track focused states
- const [focused, setFocused, clearFocused] = useSetState(() =>
- defaultsFor(configs, config => config.autoFocus && !config.disabled)
- );
- const focuses = useMemo(
- () => configs.map(config => focused[config.name] || false), //
- [configs, focused]
- );
-
- // track whether or not input has been focused
- const [hasBeenFocused, setHasBeenFocused, clearHasBeenFocused] = useSetState(
- {}
- );
-
- // track whether or not the input has been touched
- const [hasBeenTouched, setHasBeenTouched, clearHasBeenTouched] = useSetState(
- {}
- );
-
- // build fn that transforms a value by input name
- const transform = useCallback(
- (name, value) => compose(...get(byName, [name, 'transformers'], []))(value),
- [byName]
- );
-
- // set a value (and transform it)
- const setValue = useCallback(
- (name, value) => _setValues({ [name]: transform(name, value) }),
- [_setValues, transform]
- );
-
- // reset the form
- const reset = useCallback(() => {
- clearFocused();
- clearHasBeenFocused();
- clearHasBeenTouched();
- clearValues();
- }, [clearFocused, clearHasBeenFocused, clearHasBeenTouched, clearValues]);
-
- // build fn that validates a value by input name
- const validate = useCallback(
- (name, value) =>
- compose(
- ...get(byName, [name, 'validators'], []),
- kDefaultValidator
- )(value),
- [byName]
- );
-
- const getValue = useCallback(
- (name, e) => {
- if (byName[name].type === 'checkbox') {
- return e.target.checked;
- }
-
- return e.target.value;
- },
- [byName]
- );
-
- // on change, transform and set value
- const onChange = useCallback(
- name => e => {
- setValue(name, getValue(name, e));
- setHasBeenTouched({ [name]: true });
- }, //
- [setValue, getValue, setHasBeenTouched]
- );
-
- // on focus, update focus
- const onFocus = useCallback(
- name => e => setFocused({ [name]: true }), //
- [setFocused]
- );
-
- // on blur, defocus and set has been focused
- const onBlur = useCallback(
- name => e => {
- setFocused({ [name]: false });
- setHasBeenFocused({ [name]: true });
- },
- [setFocused, setHasBeenFocused]
- );
-
- // memo-compute validations of curent values
- const validations = useMemo(
- () => configs.map(config => validate(config.name, values[config.name])), //
- [configs, validate, values]
- );
-
- // memo-compute the set of (perhaps changed by validation) data
- const datas = useMemo(() => validations.map(v => v.data), [validations]);
- const syncPasses = useMemo(
- () => validations.map(v => v.pass, [validations]),
- [validations]
- );
-
- // the input has errored if it
- // 1) did not pass validation and has an error text
- // 2) or has a specific error from parent state
- // and then we store that in errorTexts[i]
- //
[]
- const errorTexts = useMemo(
- () =>
- configs.map((config, i) => validations[i].error || config.error || false),
- [configs, validations]
- );
-
- // we should hint at an error if it
- // 1) has been focused before
- // 2) and has an error
- // boolean[]
- const hintErrors = useMemo(
- () =>
- configs.map(
- (config, i) => hasBeenFocused[config.name] && !!errorTexts[i]
- ),
- [configs, errorTexts, hasBeenFocused]
- );
-
- // we should block on error (with potential reflow) if it
- // 1) has been touched
- // 2) and has an error
- // consumers should use this value to show error text and block on operations
- // []
- const errors = useMemo(
- () =>
- configs.map((config, i) => hasBeenTouched[config.name] && errorTexts[i]),
- [configs, errorTexts, hasBeenTouched]
- );
-
- // this input has passed if
- // 1) it has passed local validation
- // 2) there are no errors
- const passes = useMemo(
- () => validations.map((v, i) => v.pass && !errorTexts[i]),
- [validations, errorTexts]
- );
-
- // visibly tell the user that their input has passed if
- // 1) they are or have interacted with the input before
- // 2) and it has passed as defined above
- const visiblePasses = useMemo(
- () =>
- configs.map(
- (config, i) =>
- (focused[config.name] || hasBeenFocused[config.name]) && //
- passes[i]
- ),
- [configs, hasBeenFocused, focused, passes]
- );
-
- // generate the list of input states
- const inputs = useMemo(
- () =>
- configs.map(
- ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => {
- const value =
- datas[i] !== undefined
- ? datas[i]
- : initialValue !== undefined
- ? initialValue
- : '';
- return {
- // Input props
- name,
- value: value,
- data: passes[i] ? datas[i] : undefined,
- pass: passes[i],
- syncPass: syncPasses[i],
- visiblyPassed: visiblePasses[i],
- error: errors[i],
- hintError: hintErrors[i],
- focused: focuses[i],
- touched: hasBeenTouched[name],
- autoFocus: autoFocus && !disabled,
- disabled,
- ...rest,
- // DOM Properties for
- bind: {
- value,
- checked: !!datas[i],
- onChange: onChange(name),
- onFocus: onFocus(name),
- onBlur: onBlur(name),
- autoFocus: autoFocus && !disabled,
- },
- };
- }
- ),
- [
- configs,
- datas,
- passes,
- syncPasses,
- visiblePasses,
- errors,
- hintErrors,
- focuses,
- hasBeenTouched,
- onChange,
- onFocus,
- onBlur,
- ]
- );
-
- // did all of the inputs pass validation?
- const pass = useMemo(() => every(passes), [passes]);
- // did any of the inputs error?
- const error = useMemo(() => some(errors), [errors]);
-
- return {
- inputs,
- pass,
- error,
- setValue,
- reset,
- };
-}
diff --git a/src/indigo-react/lib/useOnClickOutside.js b/src/indigo-react/lib/useOnClickOutside.js
index 27bf10ef5..55b890780 100644
--- a/src/indigo-react/lib/useOnClickOutside.js
+++ b/src/indigo-react/lib/useOnClickOutside.js
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
// via https:// usehooks.com/useOnClickOutside/
-
export default function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = event => {
diff --git a/src/lib/isPromise.js b/src/lib/isPromise.js
new file mode 100644
index 000000000..905bfb4cc
--- /dev/null
+++ b/src/lib/isPromise.js
@@ -0,0 +1,6 @@
+// an object is a promise if it has a `.then` function
+// https://github.com/then/is-promise/blob/master/index.js
+export default obj =>
+ !!obj &&
+ (typeof obj === 'object' || typeof obj === 'function') &&
+ typeof obj.then === 'function';
diff --git a/src/lib/lib.js b/src/lib/lib.js
index 10b4fa161..de8b13055 100644
--- a/src/lib/lib.js
+++ b/src/lib/lib.js
@@ -16,6 +16,8 @@ const seq = n => Array.from(Array(n), (_, i) => i);
const fill = (n, v) => Array.from(Array(n), () => v);
+const strSplice = (arr, i, val) => `${arr.slice(0, i)}${val}${arr.slice(i)}`;
+
const isValidGalaxy = name => {
let point;
try {
@@ -48,6 +50,7 @@ export {
defaultTo,
seq,
fill,
+ strSplice,
isValidGalaxy,
randomPatq,
patpStringLength,
diff --git a/src/lib/pluralize.js b/src/lib/pluralize.js
index 8ff660873..3b6c37735 100644
--- a/src/lib/pluralize.js
+++ b/src/lib/pluralize.js
@@ -1,5 +1,5 @@
export default (count, singular, plural) => {
- const isPlural = count > 1;
+ const isPlural = count === 0 || count > 1;
if (!plural) {
return `${count} ${singular}${isPlural ? 's' : ''}`;
}
diff --git a/src/lib/transformers.js b/src/lib/transformers.js
deleted file mode 100644
index 0df6389bd..000000000
--- a/src/lib/transformers.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// import { fill } from './lib'
-
-export const prependSig = s => (s.charAt(0) !== '~' ? `~${s}` : s);
-
-export const convertToNumber = s => {
- try {
- return parseInt(s, 10);
- } catch {
- return 0;
- }
-};
-
-// const hideAllButLast = s => {
-// const ll = s[s.length -1];
-// const bs = fill(s.length - 1, '•')
-// return `${bs}${ll}`
-// };
-//
-// const hideAll = s => fill(s.length, '•');
diff --git a/src/lib/txn.js b/src/lib/txn.js
index 3562728b3..98c3cd7c4 100644
--- a/src/lib/txn.js
+++ b/src/lib/txn.js
@@ -129,13 +129,19 @@ const sendSignedTransaction = (web3, stx, doubtNonceError) => {
});
};
-// returns a Promise, where the bool indicates tx success/failure
+// returns a Promise, throwing on tx failure
const waitForTransactionConfirm = (web3, txHash) => {
return retry(async (bail, n) => {
const receipt = await web3.eth.getTransactionReceipt(txHash);
const confirmed = receipt !== null;
- if (confirmed) return receipt.status === true;
- else throw new Error('retrying');
+ if (!confirmed) {
+ throw new Error('Transaction not confirmed.');
+ }
+
+ const success = receipt.status === true;
+ if (!success) {
+ return bail(new Error('Transaction failed.'));
+ }
}, RETRY_OPTIONS);
};
diff --git a/src/lib/useArray.js b/src/lib/useArray.js
deleted file mode 100644
index 8a3214035..000000000
--- a/src/lib/useArray.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { useCallback, useState } from 'react';
-import { identity } from 'lodash';
-
-/**
- * Manages an immutable array of items.
- */
-export default function useArray(initialItems = [], itemBuilder = identity) {
- const [items, _setItems] = useState(initialItems);
-
- const append = useCallback(
- item => _setItems(items => [...items, itemBuilder(item)]),
- [_setItems, itemBuilder]
- );
-
- const removeAt = useCallback(
- i =>
- _setItems(items => {
- const newItems = [...items.slice(0, i), ...items.slice(i + 1)];
- return newItems;
- }),
- [_setItems]
- );
-
- const updateAt = useCallback(
- (i, update = {}) =>
- _setItems(items => [
- ...items.slice(0, i),
- {
- ...items[i],
- ...update,
- },
- ...items.slice(i + 1),
- ]),
- [_setItems]
- );
-
- const clear = useCallback(() => _setItems([]), [_setItems]);
-
- return [
- items,
- {
- append,
- removeAt,
- updateAt,
- clear,
- },
- ];
-}
diff --git a/src/lib/useConstant.js b/src/lib/useConstant.js
new file mode 100644
index 000000000..716717b35
--- /dev/null
+++ b/src/lib/useConstant.js
@@ -0,0 +1,13 @@
+import { useRef } from 'react';
+
+// inspired by, but simpler version of:
+// https://github.com/Andarist/use-constant/blob/master/src/index.ts
+export default function useConstant(fn) {
+ const ref = useRef();
+
+ if (!ref.current) {
+ ref.current = fn();
+ }
+
+ return ref.current;
+}
diff --git a/src/lib/useEthereumTransaction.js b/src/lib/useEthereumTransaction.js
index 4c87b02e5..47d024604 100644
--- a/src/lib/useEthereumTransaction.js
+++ b/src/lib/useEthereumTransaction.js
@@ -200,6 +200,7 @@ export default function useEthereumTransaction(
}, [estimatedGasPrice]);
useEffect(() => {
+ let mounted = true;
// if nonce or chainId is undefined, re-fetch on-chain info
if (!(nonce === undefined || chainId === undefined)) {
return;
@@ -213,16 +214,30 @@ export default function useEthereumTransaction(
_web3.eth.net.getId(),
]);
+ if (!mounted) {
+ return;
+ }
+
setNonce(nonce);
setChainId(chainId);
} catch (error) {
setError(error);
}
})();
- }, [_wallet, _web3, setError, nonce, chainId, networkType]);
+
+ return () => (mounted = false);
+ }, [
+ _wallet,
+ _web3,
+ setError,
+ nonce,
+ chainId,
+ networkType,
+ estimatedGasPrice,
+ ]);
useEffect(() => {
- let cancelled = false;
+ let mounted = true;
if (confirmed) {
(async () => {
@@ -241,7 +256,7 @@ export default function useEthereumTransaction(
}
}
- if (cancelled) {
+ if (!mounted) {
return;
}
@@ -249,7 +264,7 @@ export default function useEthereumTransaction(
})();
}
- return () => (cancelled = true);
+ return () => (mounted = false);
}, [confirmed, refetch, completed, setError]);
const values = useDeepEqualReference({
diff --git a/src/lib/useImpliedPoint.js b/src/lib/useImpliedPoint.js
index 05a42c242..015863dc2 100644
--- a/src/lib/useImpliedPoint.js
+++ b/src/lib/useImpliedPoint.js
@@ -1,13 +1,13 @@
import * as ob from 'urbit-ob';
-import { prependSig } from './transformers';
+import { ensurePatFormat } from 'form/formatters';
/**
* pull the suggested point from the subdomain
*/
export default function useImpliedPoint() {
const subdomain = window.location.host.split('.')[0];
- const patp = prependSig(subdomain);
+ const patp = ensurePatFormat(subdomain);
const isValid = ob.isValidPatp(patp);
return isValid ? patp : null;
diff --git a/src/lib/useImpliedTicket.js b/src/lib/useImpliedTicket.js
index b58b35694..748023c11 100644
--- a/src/lib/useImpliedTicket.js
+++ b/src/lib/useImpliedTicket.js
@@ -1,13 +1,13 @@
import * as ob from 'urbit-ob';
-import { prependSig } from './transformers';
+import { ensurePatFormat } from 'form/formatters';
/**
* pull the suggested ticket from the #hash in the url
*/
export default function useImpliedTicket() {
const hash = window.location.hash.slice(1);
- const ticket = prependSig(hash);
+ const ticket = ensurePatFormat(hash);
const isValid = ob.isValidPatq(ticket);
return isValid ? ticket : null;
diff --git a/src/lib/useInputs.js b/src/lib/useInputs.js
deleted file mode 100644
index bfa10c506..000000000
--- a/src/lib/useInputs.js
+++ /dev/null
@@ -1,277 +0,0 @@
-import React, { useCallback, useMemo } from 'react';
-import { AccessoryIcon, useForm } from 'indigo-react';
-import { identity } from 'lodash';
-
-import { DEFAULT_HD_PATH } from 'lib/wallet';
-import {
- validateNotEmpty,
- validateMnemonic,
- validatePoint,
- validateTicket,
- validateEmail,
- validateLength,
- validateHexString,
- validateOneOf,
- validateMaximumPatpByteLength,
- validateEthereumAddress,
- validateNotNullAddress,
- validateGreaterThan,
-} from 'lib/validators';
-import { prependSig, convertToNumber } from 'lib/transformers';
-
-import InputSigil from 'components/InputSigil';
-
-const PLACEHOLDER_POINT = '~sampel-ponnym';
-const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
-const PLACEHOLDER_MNEMONIC =
- 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
-const PLACEHOLDER_TICKET = '~sampel-ticlyt-migfun-falmel';
-const PLACEHOLDER_ADDRESS = '0x12345abcdeDB11D175F123F6891AA64F01c24F7d';
-const PLACEHOLDER_PRIVATE_KEY =
- '0x12345abcdee6beb2f323fab48b432925c9785808d33a6ca6d7ba00b45e9370c3';
-
-// pulls out the first input from a useForm() call
-function useFirstOf({ inputs, setValue, ...rest }, mapper = identity) {
- // ask the mapper function for any values to overwrite
- const input = {
- ...inputs[0],
- ...mapper(inputs[0]),
- };
- // memoize the setValue callback for manually setting the value
- // in response to some imperative event
- const _setValue = useCallback(value => setValue(input.name, value), [
- setValue,
- input.name,
- ]);
-
- // provide the first input as the first element
- // _and_ as the second element to facilitate destructuring
- // and then provide the pass/error/setValue/etc properties at the end
- return [
- input,
- input,
- {
- ...rest,
- setValue: _setValue,
- },
- ];
-}
-
-export function usePassphraseInput(props) {
- return useFirstOf(
- useForm([
- {
- type: 'password',
- autoComplete: 'off',
- placeholder: 'Passphrase',
- ...props,
- },
- ])
- );
-}
-
-export function useHexInput({ length, ...rest }) {
- return useFirstOf(
- useForm([
- {
- type: 'text', // or password
- autoComplete: 'off',
- placeholder: PLACEHOLDER_PRIVATE_KEY,
- validators: useMemo(
- () => [
- validateHexString,
- validateLength(length + 2),
- validateNotEmpty,
- ],
- [length]
- ),
- ...rest,
- },
- ])
- );
-}
-
-const kMnemonicValidators = [validateMnemonic, validateNotEmpty];
-export function useMnemonicInput(props) {
- return useFirstOf(
- useForm([
- {
- type: 'textarea',
- autoComplete: 'off',
- placeholder: PLACEHOLDER_MNEMONIC,
- validators: kMnemonicValidators,
- ...props,
- },
- ])
- );
-}
-
-export function useHdPathInput(props) {
- return useFirstOf(
- useForm([
- {
- type: 'text',
- autoComplete: 'off',
- placeholder: PLACEHOLDER_HD_PATH,
- ...props,
- },
- ])
- );
-}
-
-const kTicketValidators = [validateTicket, validateNotEmpty];
-//TODO needs to be fancier, displaying sig and dashes instead of •ing all
-const kTicketTransformers = [prependSig];
-export function useTicketInput({ validators = [], deriving = false, ...rest }) {
- return useFirstOf(
- useForm([
- {
- type: 'password',
- label: 'Ticket',
- placeholder: PLACEHOLDER_TICKET,
- validators: useMemo(() => [...validators, ...kTicketValidators], [
- validators,
- ]),
- transformers: kTicketTransformers,
- mono: true,
- ...rest,
- },
- ]),
- ({ error, pass }) => ({
- accessory: error ? (
-
- ) : deriving ? (
-
- ) : pass ? (
-
- ) : null,
- })
- );
-}
-
-const kPointTransformers = [prependSig];
-export function usePointInput({ size = 4, validators = [], ...rest }) {
- const _validators = useMemo(
- () => [
- ...validators,
- validatePoint,
- validateMaximumPatpByteLength(size),
- validateNotEmpty,
- ],
- [size, validators]
- );
-
- return useFirstOf(
- useForm([
- {
- type: 'text',
- label: 'Point',
- placeholder: PLACEHOLDER_POINT,
- validators: _validators,
- transformers: kPointTransformers,
- mono: true,
- ...rest,
- },
- ]),
- ({ error, pass, focused, value }) => ({
- accessory: value ? (
-
- ) : null,
- })
- );
-}
-
-export function useGalaxyInput(props) {
- return usePointInput({
- label: 'Galaxy Name',
- size: 1,
- ...props,
- });
-}
-
-export function useCheckboxInput({ initialValue, ...rest }) {
- return useFirstOf(
- useForm([
- {
- type: 'checkbox',
- ...rest,
- },
- ])
- );
-}
-
-export function useSelectInput({ initialValue, options, ...rest }) {
- return useFirstOf(
- useForm([
- {
- type: 'select',
- validators: useMemo(
- () => [
- validateOneOf(
- options.map(option => option.value),
- validateNotEmpty
- ),
- ],
- [options]
- ),
- options,
- initialValue: initialValue || options[0].value,
- ...rest,
- },
- ])
- );
-}
-
-const kAddressValidators = [
- validateEthereumAddress,
- validateNotNullAddress,
- validateNotEmpty,
-];
-export function useAddressInput({ ...rest }) {
- return useFirstOf(
- useForm([
- {
- type: 'string',
- label: 'Ethereum Address',
- placeholder: PLACEHOLDER_ADDRESS,
- autoComplete: 'off',
- validators: kAddressValidators,
- mono: true,
- ...rest,
- },
- ])
- );
-}
-
-const kNumberTransformers = [convertToNumber];
-const kNumberValidators = [validateGreaterThan(0)];
-export function useNumberInput({ ...rest }) {
- return useFirstOf(
- useForm([
- {
- type: 'number',
- label: 'Number',
- autoComplete: 'off',
- transformers: kNumberTransformers,
- validators: kNumberValidators,
- ...rest,
- },
- ])
- );
-}
-
-const kEmailValidators = [validateEmail, validateNotEmpty];
-export const buildEmailInputConfig = extra => ({
- type: 'email',
- autoComplete: 'off',
- validators: kEmailValidators,
- initialValue: '',
- ...extra,
-});
diff --git a/src/lib/useMailer.js b/src/lib/useMailer.js
index 7fc9a21bc..a947c0932 100644
--- a/src/lib/useMailer.js
+++ b/src/lib/useMailer.js
@@ -1,43 +1,29 @@
-import { useCallback } from 'react';
-import { Just, Nothing } from 'folktale/maybe';
+import { useCallback, useRef } from 'react';
import { hasReceived, sendMail } from './inviteMail';
-import useSetState from './useSetState';
+import timeout from './timeout';
const STUB_MAILER = process.env.REACT_APP_STUB_MAILER === 'true';
-function useHasReceivedCache() {
- const [cache, addToCache] = useSetState();
+export default function useMailer() {
+ const cache = useRef({});
const getHasReceived = useCallback(
- email => cache[email] || Nothing(), //
- [cache]
- );
-
- const syncHasReceivedForEmail = useCallback(
async email => {
- if (Just.hasInstance(getHasReceived(email))) {
- // never update the cache after we know about it
- return;
- }
-
- if (STUB_MAILER) {
- // always allow sending emails when stubbing
- return addToCache({ [email]: Just(false) });
+ if (!cache.current[email]) {
+ if (STUB_MAILER) {
+ await timeout(350); // simulate request
+ cache.current[email] = false;
+ } else {
+ cache.current[email] = await hasReceived(email);
+ }
}
- const _hasReceived = await hasReceived(email);
- addToCache({ [email]: Just(_hasReceived) });
+ return cache.current[email];
},
- [getHasReceived, addToCache]
+ [cache]
);
- return { getHasReceived, syncHasReceivedForEmail };
-}
-
-export default function useMailer(emails) {
- const hasReceivedCache = useHasReceivedCache(emails);
-
// prefix to avoid clobbering sendMail import
// also throws if return value is false
const _sendMail = useCallback(async (email, ticket, sender, rawTx) => {
@@ -46,8 +32,11 @@ export default function useMailer(emails) {
return true;
}
- return await sendMail(email, ticket, sender, rawTx);
+ const success = await sendMail(email, ticket, sender, rawTx);
+ if (!success) {
+ throw new Error('Failed to send mail');
+ }
}, []);
- return { ...hasReceivedCache, sendMail: _sendMail };
+ return { getHasReceived, sendMail: _sendMail };
}
diff --git a/src/lib/useRouter.js b/src/lib/useRouter.js
index 060c5aff0..c5efd231b 100644
--- a/src/lib/useRouter.js
+++ b/src/lib/useRouter.js
@@ -53,7 +53,7 @@ export default function useRouter({
if (size <= 1) {
// if we are at the root, pass this event to our parent
if (oldPopState.current) {
- window.history.back();
+ oldPopState.current();
}
return;
}
@@ -108,7 +108,7 @@ export default function useRouter({
// construct new onpopstate handler
window.onpopstate = e => {
- e.stopImmediatePropagation();
+ e && e.stopImmediatePropagation();
pop();
};
diff --git a/src/lib/validators.js b/src/lib/validators.js
index b9a14223e..6409a2479 100644
--- a/src/lib/validators.js
+++ b/src/lib/validators.js
@@ -1,6 +1,6 @@
import * as bip39 from 'bip39';
import * as ob from 'urbit-ob';
-import { identity, includes } from 'lodash';
+import { includes } from 'lodash';
import { isValidAddress, ETH_ZERO_ADDR, ETH_ZERO_ADDR_SHORT } from './wallet';
import patp2dec from './patp2dec';
@@ -14,261 +14,83 @@ import { MIN_GALAXY, MAX_GALAXY } from './constants';
// via: https://emailregex.com/
const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-// is this 64 hex chars?
-const plain64CharHexValue = /[0-9A-Fa-f]{64}/;
-
// is this only hex values with a 0x prefix?
const isHexString = /0x[0-9A-Fa-f]/;
-// Wraps single validation functions in a controlled and predictable way.
-export const simpleValidatorWrapper = ({
- prevMessage,
- transform = identity,
- validator,
- error,
-}) => {
- // If a previous validation has already failed, skip this validation and
- // return the prev message to the next stage in the validation function chain.
- // Failed validations should drop all the way down the chain and drop out of
- // the output.
- if (!prevMessage.pass) {
- return prevMessage;
- }
-
- // Run the validator and return the result.
- const pass = validator(prevMessage.data);
- const data = pass ? transform(prevMessage.data) : prevMessage.data;
-
- return newMessage(data, pass, !pass && error);
-};
-
-// Validation message
-// {
-// data: ...
-// pass: ...
-// error: ...
-// }
-// Creates a new validation message in a uniform way.
-const newMessage = (data, pass, error) => ({
- // The input data
- data,
- // Has the data passed validation?
- pass,
- // If data has failed a validator, the error message goes here.
- error,
-});
-
-// A validator that always passes.
-export const kDefaultValidator = data => newMessage(data, true, null);
-
// Validates a bip39 mnemonic
-export const validateMnemonic = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => bip39.validateMnemonic(d),
- error: 'This is not a valid mnemonic.',
- });
+export const validateMnemonic = v =>
+ !bip39.validateMnemonic(v) && 'This is not a valid mnemonic.';
// Checks an empty field
-export const validateNotEmpty = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => {
- try {
- return d.length > 0;
- } catch {
- return false;
- }
- },
- error: 'This field is required.',
- });
+export const validateNotEmpty = v =>
+ (v === undefined || v.length === 0) && 'This field is required.';
// Checks if a patp is a valid galaxy
-export const validateGalaxy = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => {
- let point;
- try {
- point = patp2dec(d);
- return point >= MIN_GALAXY && point <= MAX_GALAXY;
- } catch (e) {
- return false;
- }
- },
- error: 'This is not a valid galaxy.',
- });
-
-export const validatePoint = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => {
- try {
- return ob.isValidPatp(d);
- } catch (e) {
- return false;
- }
- },
- error: 'This is not a valid point.',
- });
-
-export const validateTicket = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => {
- try {
- return ob.isValidPatq(d);
- } catch (e) {
- return false;
- }
- },
- error: 'This is not a valid ticket.',
- });
-
-export const validateShard = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => {
- try {
- return ob.isValidPatq(d);
- } catch (e) {
- return false;
- }
- },
- error: 'This is not a valid shard.',
- });
-
-export const validateOneOf = (options = []) => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => includes(options, d),
- error: `Is not a valid option.`,
- });
-
-export const validateHexString = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => isHexString.test(d),
- error: 'This is not a valid hex string.',
- });
+export const validateGalaxy = v => {
+ try {
+ const point = patp2dec(v);
+ const isValidGalaxy = point >= MIN_GALAXY && point <= MAX_GALAXY;
+ if (!isValidGalaxy) {
+ throw new Error();
+ }
+ } catch {
+ return 'This is not a valid galaxy.';
+ }
+};
-// @deprecate
-export const validateNetworkKey = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => plain64CharHexValue.test(d),
- error: 'This is not a valid network key.',
- });
+export const validatePoint = v => {
+ try {
+ if (!ob.isValidPatp(v)) {
+ throw new Error();
+ }
+ } catch {
+ return 'This is not a valid point.';
+ }
+};
-// @deprecate
-export const validateNetworkSeed = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => plain64CharHexValue.test(d),
- error: 'This is not a valid network seed.',
- });
+export const validatePatq = v => {
+ try {
+ if (!ob.isValidPatq(v)) {
+ throw new Error();
+ }
+ } catch {
+ return 'This is not a valid ticket.';
+ }
+};
-// Checks if a string is a valid ethereum address
-export const validateEthereumAddress = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => isValidAddress(d),
- error: 'This is not a valid Ethereum address.',
- });
+export const validateOneOf = (options = []) => v =>
+ !includes(options, v) && 'Is not a valid option.';
-export const validateEmail = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => emailRegExp.test(d),
- error: 'This is not a valid email address.',
- });
+export const validateHexString = v =>
+ !isHexString.test(v) && 'This is not a valid hex string.';
-export const validateInt = m =>
- simpleValidatorWrapper({
- prevMessage: m,
- transform: d => parseInt(d, 10),
- validator: d => {
- try {
- parseInt(d, 10);
- return;
- } catch {
- return false;
- }
- },
- error: 'This is not a valid number.',
- });
+export const validateEthereumAddress = v =>
+ !isValidAddress(v) && 'This is not a valid Ethereum address.';
-export const validateExactly = (value, error) => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => d === value,
- error,
- });
+export const validateEmail = v =>
+ !emailRegExp.test(v) && 'This is not a valid email address.';
-export const validateNotAny = (values = []) => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => !values.includes(d),
- error: `Cannot be ${m.data}.`,
- });
+export const validateExactly = (value, error) => v => v !== value && error;
-export const validateLength = l => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => {
- try {
- return d.length === l;
- } catch {
- return false;
- }
- },
- error: `Must be exactly ${l} characters.`,
- });
+export const validateNotAny = (values = []) => v =>
+ values.includes(v) && `Cannot be ${v}.`;
-export const validateMaximumLength = l => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => {
- try {
- return d.length <= l;
- } catch {
- return false;
- }
- },
- error: `Must be ${l} characters or fewer.`,
- });
+export const validateLength = l => v =>
+ v.length !== l && `Must be exactly ${l} characters.`;
-export const validateGreaterThan = l => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => d > l,
- error: `Must be at least ${l}.`,
- });
+export const validateHexLength = l => v =>
+ v.length !== l + 2 && `Must be exactly ${l} hex characters.`;
-export const validateInSet = (set, error) => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: d => set.has(d),
- error,
- });
+export const validateMaximumLength = l => v =>
+ v.length > l && `Must be ${l} characters or fewer.`;
-export const validateNameInNumberSet = (set, error) => m =>
- simpleValidatorWrapper({
- prevMessage: m,
- validator: p => {
- const num = patp2dec(p);
- return set.has(num);
- },
- error,
- });
+export const validateGreaterThan = l => v =>
+ !(v > l) && `Must be at least ${l}.`;
-export const validatePatpByteLength = byteLength => {
- return validateLength(patpStringLength(byteLength));
-};
+export const validateInSet = (set, error) => v => !set.has(v) && error;
-export const validateMaximumPatpByteLength = byteLength => {
- return validateMaximumLength(patpStringLength(byteLength));
-};
+export const validateMaximumPatpByteLength = byteLength =>
+ validateMaximumLength(patpStringLength(byteLength));
export const validateNotNullAddress = validateNotAny([
ETH_ZERO_ADDR,
diff --git a/src/store/wallet.js b/src/store/wallet.js
index ead08cf15..9533e2ffa 100644
--- a/src/store/wallet.js
+++ b/src/store/wallet.js
@@ -33,7 +33,12 @@ const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET;
// JSON keystore files, and Metamask authentication, it wraps an
// 'EthereumWallet'.
function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) {
- const [walletType, _setWalletType] = useState(DEFAULT_WALLET_TYPE);
+ const [walletType, _setWalletType] = useState(() =>
+ initialMnemonic.matchWith({
+ Nothing: () => DEFAULT_WALLET_TYPE,
+ Just: () => WALLET_TYPES.MNEMONIC,
+ })
+ );
const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH);
const [wallet, _setWallet] = useState(initialWallet);
const [urbitWallet, _setUrbitWallet] = useState(Nothing());
diff --git a/src/style/indigo-proposals.scss b/src/style/indigo-proposals.scss
index f14497e29..0188a91be 100644
--- a/src/style/indigo-proposals.scss
+++ b/src/style/indigo-proposals.scss
@@ -37,6 +37,10 @@
user-select: none;
}
+.bs-none {
+ box-shadow: none !important;
+}
+
.auto-rows-min {
grid-auto-rows: min-content;
}
diff --git a/src/views/Activate.js b/src/views/Activate.js
index 6ce7bf165..e662fc01f 100644
--- a/src/views/Activate.js
+++ b/src/views/Activate.js
@@ -1,11 +1,12 @@
import React from 'react';
import useRouter from 'lib/useRouter';
+import { LocalRouterProvider } from 'lib/LocalRouter';
+import { useSyncKnownPoints } from 'lib/useSyncPoints';
import { ActivateFlowProvider } from './Activate/ActivateFlow';
import useActivateFlowState from './Activate/useActivateFlowState';
import ActivateCode from './Activate/ActivateCode';
-import { LocalRouterProvider } from 'lib/LocalRouter';
import ActivatePassport from './Activate/ActivatePassport';
import Disclaimer from './Disclaimer';
@@ -29,6 +30,11 @@ export default function Activate() {
initialRoutes: [{ key: NAMES.CODE }],
});
+ // when we know the derived point, ensure we have the data to display it
+ useSyncKnownPoints(
+ [state.derivedPoint.getOrElse(null)].filter(p => p !== null)
+ );
+
return (
diff --git a/src/views/Activate/ActivateCode.js b/src/views/Activate/ActivateCode.js
index 5f6c69b42..ff359609c 100644
--- a/src/views/Activate/ActivateCode.js
+++ b/src/views/Activate/ActivateCode.js
@@ -1,30 +1,41 @@
-import React, { useCallback, useEffect, useState } from 'react';
-import { Just, Nothing } from 'folktale/maybe';
+import React, { useCallback, useMemo, useRef } from 'react';
+import { Just } from 'folktale/maybe';
import * as azimuth from 'azimuth-js';
-import { Grid, Input, H4, ErrorText } from 'indigo-react';
+import { Grid, H4 } from 'indigo-react';
+import { FORM_ERROR } from 'final-form';
import View from 'components/View';
import { ForwardButton } from 'components/Buttons';
import Passport from 'components/Passport';
+import WarningBox from 'components/WarningBox';
+import FooterButton from 'components/FooterButton';
+import { useNetwork } from 'store/network';
import { useHistory } from 'store/history';
-import { useTicketInput } from 'lib/useInputs';
import * as need from 'lib/need';
import { ROUTE_NAMES } from 'lib/routeNames';
-import { useSyncKnownPoints } from 'lib/useSyncPoints';
-import FooterButton from 'components/FooterButton';
-import { blinkIf } from 'components/Blinky';
import { DEFAULT_HD_PATH, walletFromMnemonic } from 'lib/wallet';
-import { useNetwork } from 'store/network';
import { generateWallet } from 'lib/invite';
import { generateTemporaryOwnershipWallet } from 'lib/walletgen';
-import { useActivateFlow } from './ActivateFlow';
import { useLocalRouter } from 'lib/LocalRouter';
import useImpliedTicket from 'lib/useImpliedTicket';
+import timeout from 'lib/timeout';
import useHasDisclaimed from 'lib/useHasDisclaimed';
import useBreakpoints from 'lib/useBreakpoints';
-import WarningBox from 'components/WarningBox';
+
+import BridgeForm from 'form/BridgeForm';
+import SubmitButton from 'form/SubmitButton';
+import { TicketInput } from 'form/Inputs';
+import {
+ composeValidator,
+ buildPatqValidator,
+ hasErrors,
+} from 'form/validators';
+import FormError from 'form/FormError';
+
+import { useActivateFlow } from './ActivateFlow';
+import { WARNING } from 'form/helpers';
export default function ActivateCode() {
const history = useHistory();
@@ -32,10 +43,9 @@ export default function ActivateCode() {
const { contracts } = useNetwork();
const impliedTicket = useImpliedTicket();
const [hasDisclaimed] = useHasDisclaimed();
- const [generalError, setGeneralError] = useState();
- const [deriving, setDeriving] = useState(false);
+ const didWarn = useRef(false);
+
const {
- derivedWallet,
setDerivedWallet,
setInviteWallet,
derivedPoint,
@@ -48,129 +58,134 @@ export default function ActivateCode() {
// for this screen
const activationAllowed = useBreakpoints([false, true, true]);
- const [ticketInput, { pass: validTicket, data: ticket }] = useTicketInput({
- name: 'ticket',
- label: 'Activation Code',
- initialValue: impliedTicket || '',
- autoFocus: true,
- disabled: !activationAllowed,
- });
-
const goToLogin = useCallback(() => history.popAndPush(ROUTE_NAMES.LOGIN), [
history,
]);
+
const goToPassport = useCallback(() => {
push(names.PASSPORT);
+
if (!hasDisclaimed) {
push(names.DISCLAIMER);
}
- }, [names, push, hasDisclaimed]);
+ }, [hasDisclaimed, names.DISCLAIMER, names.PASSPORT, push]);
- const pass = derivedWallet.matchWith({
- Nothing: () => false,
- Just: () => true,
- });
+ const validateForm = useCallback((values, errors) => {
+ didWarn.current = false;
- useEffect(() => {
- if (validTicket) {
- const _contracts = need.contracts(contracts);
+ if (hasErrors(errors)) {
+ return errors;
+ }
+ }, []);
- setDeriving(true);
- // when the ticket becomes valid, derive the point
- (async () => {
- const { seed } = await generateTemporaryOwnershipWallet(ticket);
-
- //TODO isn't all this accessible in the ownership object?
- const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
- const _inviteWallet = need.wallet(inviteWallet);
-
- const owned = await azimuth.azimuth.getOwnedPoints(
- _contracts,
- _inviteWallet.address
- );
- const transferring = await azimuth.azimuth.getTransferringFor(
- _contracts,
- _inviteWallet.address
- );
- const incoming = [...owned, ...transferring];
-
- let realPoint = Nothing();
- let wallet = Nothing();
-
- if (incoming.length > 0) {
- const pointNum = parseInt(incoming[0], 10);
- realPoint = Just(pointNum);
- wallet = Just(await generateWallet(pointNum));
-
- if (incoming.length > 1) {
- setGeneralError(
- 'This invite code has multiple points available.\n' +
- "Once you've activated this point, activate the next with the same process."
- );
- } else {
- setGeneralError(false);
- }
- } else {
- setGeneralError(
- 'Invite code has no claimable point.\n' +
- 'Check your invite code and try again?'
- );
+ const validate = useMemo(
+ () => composeValidator({ ticket: buildPatqValidator() }, validateForm),
+ [validateForm]
+ );
+
+ // set our state on submission
+ const onSubmit = useCallback(
+ async values => {
+ await timeout(16); // allow the ui changes to flush before we lag it out
+
+ const _contracts = need.contracts(contracts);
+ const { seed } = await generateTemporaryOwnershipWallet(values.ticket);
+
+ const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
+
+ const _inviteWallet = need.wallet(inviteWallet);
+
+ const owned = await azimuth.azimuth.getOwnedPoints(
+ _contracts,
+ _inviteWallet.address
+ );
+ const transferring = await azimuth.azimuth.getTransferringFor(
+ _contracts,
+ _inviteWallet.address
+ );
+ const incoming = [...owned, ...transferring];
+
+ if (incoming.length > 0) {
+ if (incoming.length > 1 && !didWarn.current) {
+ didWarn.current = true;
+ return {
+ [WARNING]:
+ 'This invite code has multiple points available. ' +
+ "Once you've activated this point, " +
+ 'activate the next with the same process.',
+ };
}
- setDerivedPoint(realPoint);
- setDerivedWallet(wallet);
+ const point = parseInt(incoming[0], 10);
+
+ setDerivedPoint(Just(point));
setInviteWallet(inviteWallet);
- setDeriving(false);
- })();
- } else {
- setGeneralError(false);
- }
- }, [
- validTicket,
- contracts,
- ticket,
- setDerivedPoint,
- setDerivedWallet,
- setInviteWallet,
- ]);
+ setDerivedWallet(Just(await generateWallet(point)));
+ } else {
+ return {
+ [FORM_ERROR]:
+ 'Invite code has no claimable point.\n' +
+ 'Check your invite code and try again?',
+ };
+ }
+ },
+ [contracts, setDerivedPoint, setDerivedWallet, setInviteWallet]
+ );
- // when we know the derived point, ensure we have the data to display it
- useSyncKnownPoints([derivedPoint.getOrElse(null)].filter(p => p !== null));
+ const initialValues = useMemo(() => ({ ticket: impliedTicket || '' }), [
+ impliedTicket,
+ ]);
return (
-
-
+
+
Activate
-
-
-
- {generalError && (
-
- {generalError}
-
- )}
-
-
- {deriving && 'Deriving...'}
- {!deriving && 'Go'}
-
-
- {!activationAllowed && (
-
- For your security, please access Bridge on a desktop device.
-
- )}
+
+ {({ validating, submitting, handleSubmit }) => (
+ <>
+
+
+
+
+
+ {isWarning =>
+ validating
+ ? 'Deriving...'
+ : submitting
+ ? 'Generating...'
+ : isWarning
+ ? 'Continue Activation'
+ : 'Go'
+ }
+
+
+ {!activationAllowed && (
+
+ For your security, please access Bridge on a desktop device.
+
+ )}
+ >
+ )}
+
+
Login
diff --git a/src/views/Activate/PassportTransfer.js b/src/views/Activate/PassportTransfer.js
index 780f22760..5a89a7f46 100644
--- a/src/views/Activate/PassportTransfer.js
+++ b/src/views/Activate/PassportTransfer.js
@@ -57,11 +57,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
const [needFunds, setNeedFunds] = useState();
const goToLogin = useCallback(
- () =>
- replaceWith([
- { key: names.LOGIN },
- { key: names.POINT },
- ]),
+ () => replaceWith([{ key: names.LOGIN }, { key: names.POINT }]),
[replaceWith, names]
);
diff --git a/src/views/Activate/PassportVerify.js b/src/views/Activate/PassportVerify.js
index d0c514f9b..18c3d0500 100644
--- a/src/views/Activate/PassportVerify.js
+++ b/src/views/Activate/PassportVerify.js
@@ -1,16 +1,19 @@
import React, { useCallback, useMemo } from 'react';
-import { Grid, Input, P } from 'indigo-react';
+import { Grid, P } from 'indigo-react';
import * as need from 'lib/need';
import { useLocalRouter } from 'lib/LocalRouter';
-import { useTicketInput } from 'lib/useInputs';
import { validateExactly } from 'lib/validators';
import { isDevelopment } from 'lib/flags';
-import { ForwardButton } from 'components/Buttons';
+import SubmitButton from 'form/SubmitButton';
+import { TicketInput } from 'form/Inputs';
+import { composeValidator, buildPatqValidator } from 'form/validators';
+import BridgeForm from 'form/BridgeForm';
import { useActivateFlow } from './ActivateFlow';
import PassportView from './PassportView';
+import FormError from 'form/FormError';
const STUB_VERIFY_TICKET = isDevelopment;
@@ -20,17 +23,22 @@ export default function PassportVerify({ className }) {
const goToTransfer = useCallback(() => push(names.TRANSFER), [push, names]);
const { ticket } = need.wallet(derivedWallet);
- const validators = useMemo(
- () => [validateExactly(ticket, 'Does not match expected master ticket.')],
+ const validate = useMemo(
+ () =>
+ composeValidator({
+ ticket: buildPatqValidator([
+ validateExactly(ticket, 'Does not match expected master ticket.'),
+ ]),
+ }),
+ [ticket]
+ );
+
+ const initialValues = useMemo(
+ () => ({
+ ticket: STUB_VERIFY_TICKET ? ticket : undefined,
+ }),
[ticket]
);
- const [ticketInput, { pass }] = useTicketInput({
- name: 'ticket',
- label: 'Master Ticket',
- initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
- autoFocus: true,
- validators,
- });
return (
@@ -40,16 +48,31 @@ export default function PassportVerify({ className }) {
should be a folder of image files. One of them is your Master Ticket.
Open it and enter the 4 word phrase below (with hyphens).
-
-
- Verify
-
+
+ {({ handleSubmit }) => (
+ <>
+
+
+
+
+
+ Verify
+
+ >
+ )}
+
);
diff --git a/src/views/Admin/AdminNetworkingKeys.js b/src/views/Admin/AdminNetworkingKeys.js
index 3e7b22b33..a5866a8e8 100644
--- a/src/views/Admin/AdminNetworkingKeys.js
+++ b/src/views/Admin/AdminNetworkingKeys.js
@@ -1,14 +1,6 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Just, Nothing } from 'folktale/maybe';
-import {
- Grid,
- Text,
- H5,
- Flex,
- ToggleInput,
- Input,
- CheckboxInput,
-} from 'indigo-react';
+import { Grid, Text, H5, Flex, ToggleInput, CheckboxInput } from 'indigo-react';
import * as azimuth from 'azimuth-js';
import { randomHex } from 'web3-utils';
@@ -30,7 +22,6 @@ import { formatDotsWithTime } from 'lib/dateFormat';
import useEthereumTransaction from 'lib/useEthereumTransaction';
import { GAS_LIMITS } from 'lib/constants';
import { addHexPrefix } from 'lib/wallet';
-import { useHexInput, useCheckboxInput } from 'lib/useInputs';
import useKeyfileGenerator from 'lib/useKeyfileGenerator';
import ViewHeader from 'components/ViewHeader';
@@ -40,6 +31,16 @@ import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
import NoticeBox from 'components/NoticeBox';
+import { HexInput } from 'form/Inputs';
+import {
+ composeValidator,
+ buildCheckboxValidator,
+ buildHexValidator,
+} from 'form/validators';
+import BridgeForm from 'form/BridgeForm';
+import Condition from 'form/Condition';
+import FormError from 'form/FormError';
+
const chainKeyProp = name => d =>
d[name] === CURVE_ZERO_ADDR ? Nothing() : Just(d[name]);
@@ -163,58 +164,50 @@ export default function AdminNetworkingKeys() {
keyfileBind,
} = useSetKeys();
- const [showNetworkSeedInput, { data: showNetworkSeed }] = useCheckboxInput({
- name: 'shownetworkseed',
- label: 'Use Custom Network Seed',
- inverseLabel: 'Back to Derived Network Seed',
- initialValue: false,
- disabled: inputsLocked,
- });
+ const validateForm = useCallback((values, errors) => {
+ if (values.useNetworkSeed && errors.networkSeed) {
+ return errors;
+ }
+ }, []);
+
+ const validate = useMemo(
+ () =>
+ composeValidator(
+ {
+ useNetworkSeed: buildCheckboxValidator(),
+ networkSeed: buildHexValidator(32),
+ useDiscontinuity: buildCheckboxValidator(),
+ },
+ validateForm
+ ),
+ [validateForm]
+ );
- const [
- networkSeedInput,
- { pass: validNetworkSeed, data: networkSeed },
- { reset: resetNetworkSeed },
- ] = useHexInput({
- name: 'networkseed',
- label: 'Network Seed (64 bytes)',
- length: 32, // 64 bytes
- disabled: inputsLocked,
- });
+ const onValues = useCallback(
+ ({ valid, values, form }) => {
+ if (valid) {
+ construct(
+ values.useNetworkSeed ? values.networkSeed : undefined,
+ values.useDiscontinuity
+ );
+ } else {
+ unconstruct();
+ }
- const [
- discontinuityInput,
- { pass: validDiscontinuity, data: isDiscontinuity },
- ] = useCheckboxInput({
- name: 'discontinuity',
- label: 'Trigger New Continuity Era',
- initialValue: false,
- disabled: inputsLocked,
- });
+ if (!values.useNetworkSeed && values.networkSeed) {
+ form.change('networkSeed', '');
+ }
+ },
+ [construct, unconstruct]
+ );
- useEffect(() => {
- const nothingOrValidSeed =
- !showNetworkSeed || (showNetworkSeed && validNetworkSeed);
- if (nothingOrValidSeed && validDiscontinuity) {
- construct(networkSeed, isDiscontinuity);
- } else {
- unconstruct();
- }
- }, [
- construct,
- unconstruct,
- isDiscontinuity,
- networkSeed,
- showNetworkSeed,
- validDiscontinuity,
- validNetworkSeed,
- ]);
-
- useEffect(() => {
- if (!showNetworkSeed) {
- resetNetworkSeed();
- }
- }, [resetNetworkSeed, showNetworkSeed]);
+ const initialValues = useMemo(
+ () => ({
+ useNetworkSeed: false,
+ useDiscontinuity: false,
+ }),
+ []
+ );
const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
@@ -241,16 +234,16 @@ export default function AdminNetworkingKeys() {
{key.matchWith({
Nothing: () => (
-
+
Unset
),
Just: ({ value: key }) => (
<>
-
+
0x
-
+
{renderNetworkKey(key)}
>
@@ -335,30 +328,56 @@ export default function AdminNetworkingKeys() {
)}
{!completed && (
- <>
-
- {showNetworkSeed && (
+
+ {({ handleSubmit }) => (
<>
-
- When using a custom network seed, you'll need to download your
- Arvo keyfile immediately after this transaction is completed —
- Bridge does not store your seed.
-
-
+
+
+
+ When using a custom network seed, you'll need to download
+ your Arvo keyfile immediately after this transaction is
+ Bridge does not store your seed.
+
+
+
+
+
+
+
+ pop()}
+ />
>
)}
-
- >
+
)}
- pop()}
- />
-
{isDefaultState && renderDetails()}
{completed && (
diff --git a/src/views/Admin/AdminSetProxy.js b/src/views/Admin/AdminSetProxy.js
index d30124f5c..bc0450c2d 100644
--- a/src/views/Admin/AdminSetProxy.js
+++ b/src/views/Admin/AdminSetProxy.js
@@ -1,6 +1,6 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useMemo } from 'react';
import cn from 'classnames';
-import { Grid, Text, Input, Flex, ToggleInput } from 'indigo-react';
+import { Grid, Text, Flex, ToggleInput } from 'indigo-react';
import * as azimuth from 'azimuth-js';
import { useNetwork } from 'store/network';
@@ -16,12 +16,19 @@ import * as need from 'lib/need';
import { useLocalRouter } from 'lib/LocalRouter';
import { ETH_ZERO_ADDR, eqAddr, isZeroAddress } from 'lib/wallet';
import capitalize from 'lib/capitalize';
-import { useAddressInput, useCheckboxInput } from 'lib/useInputs';
import useEthereumTransaction from 'lib/useEthereumTransaction';
import ViewHeader from 'components/ViewHeader';
import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
import { GAS_LIMITS } from 'lib/constants';
+import { AddressInput } from 'form/Inputs';
+import {
+ composeValidator,
+ buildCheckboxValidator,
+ buildAddressValidator,
+} from 'form/validators';
+import BridgeForm from 'form/BridgeForm';
+import FormError from 'form/FormError';
const proxyFromDetails = (details, contracts, proxyType) => {
switch (proxyType) {
@@ -107,41 +114,41 @@ export default function AdminSetProxy() {
bind,
} = useSetProxy(data.proxyType);
- const [unsetInput, { data: isUnsetting }] = useCheckboxInput({
- name: 'unset',
- label: 'Unset',
- inverseLabel: 'Specify',
- initialValue: false,
- disabled: inputsLocked,
- });
- const [
- addressInput,
- { pass: validAddress, data: address },
- { reset: resetAddress },
- ] = useAddressInput({
- name: 'address',
- label: `New ${properProxyType} Address`,
- disabled: inputsLocked || isUnsetting,
- });
-
- useEffect(() => {
- if (isUnsetting) {
- unset();
- resetAddress();
- } else if (validAddress) {
- construct(address);
- } else {
- unconstruct();
+ const validateForm = useCallback((values, errors) => {
+ if (!values.unset && errors.address) {
+ return errors;
}
- }, [
- validAddress,
- address,
- construct,
- isUnsetting,
- unset,
- unconstruct,
- resetAddress,
- ]);
+ }, []);
+
+ const validate = useMemo(
+ () =>
+ composeValidator(
+ {
+ unset: buildCheckboxValidator(),
+ address: buildAddressValidator(),
+ },
+ validateForm
+ ),
+ [validateForm]
+ );
+
+ const onValues = useCallback(
+ ({ valid, values, form }) => {
+ if (valid) {
+ if (values.unset) {
+ unset();
+ form.change('address', '');
+ } else {
+ construct(values.address);
+ }
+ } else {
+ unconstruct();
+ }
+ },
+ [construct, unconstruct, unset]
+ );
+
+ const initialValues = useMemo(() => ({ unset: false }), []);
const proxyAddress = proxyFromDetails(_details, _contracts, data.proxyType);
const isProxySet = !isZeroAddress(proxyAddress);
@@ -169,34 +176,58 @@ export default function AdminSetProxy() {
{proxyAddressLabel}
-
-
- {isProxySet ? proxyAddress : 'Unset'}
-
- {!completed && isProxySet && (
-
+
+ {({ handleSubmit, values }) => (
+ <>
+
+
+ {isProxySet ? proxyAddress : 'Unset'}
+
+ {!completed && isProxySet && (
+
+ )}
+
+
+ {completed ? (
+
+ ) : (
+
+ )}
+
+
+
+ pop()}
+ />
+ >
)}
-
-
- {completed ? (
-
- ) : (
-
- )}
-
- pop()}
- />
+
);
}
diff --git a/src/views/Admin/AdminTransfer.js b/src/views/Admin/AdminTransfer.js
index 9e38cb2de..e306884da 100644
--- a/src/views/Admin/AdminTransfer.js
+++ b/src/views/Admin/AdminTransfer.js
@@ -1,6 +1,6 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useMemo } from 'react';
import cn from 'classnames';
-import { Grid, Text, Input } from 'indigo-react';
+import { Grid, Text } from 'indigo-react';
import * as azimuth from 'azimuth-js';
import { useNetwork } from 'store/network';
@@ -9,13 +9,16 @@ import { usePointCache } from 'store/pointCache';
import * as need from 'lib/need';
import { useLocalRouter } from 'lib/LocalRouter';
-import { useAddressInput } from 'lib/useInputs';
import useCurrentPointName from 'lib/useCurrentPointName';
import useEthereumTransaction from 'lib/useEthereumTransaction';
import { GAS_LIMITS } from 'lib/constants';
import ViewHeader from 'components/ViewHeader';
import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
+import { AddressInput } from 'form/Inputs';
+import { composeValidator, buildAddressValidator } from 'form/validators';
+import BridgeForm from 'form/BridgeForm';
+import FormError from 'form/FormError';
function useTransfer() {
const { contracts } = useNetwork();
@@ -47,46 +50,64 @@ export default function AdminTransfer() {
bind,
} = useTransfer();
- const [addressInput, { pass, data: address }] = useAddressInput({
- name: 'address',
- label: `Ethereum Address`,
- disabled: inputsLocked,
- });
+ const validate = useMemo(
+ () => composeValidator({ address: buildAddressValidator() }),
+ []
+ );
- useEffect(() => {
- if (pass) {
- construct(address);
- } else {
- unconstruct();
- }
- }, [pass, address, construct, unconstruct]);
+ const onValues = useCallback(
+ ({ valid, values }) => {
+ if (valid) {
+ construct(values.address);
+ } else {
+ unconstruct();
+ }
+ },
+ [construct, unconstruct]
+ );
return (
Transfer Point
-
- {completed
- ? `${address} is now the Transfer Proxy for ${name} and can accept the transfer by logging into Bridge themselves. Until they accept your transfer, you will still have ownership over ${name}.`
- : `Transfer ${name} to a new owner.`}
-
- {!completed && (
-
- )}
+
+ {({ handleSubmit, values }) => (
+ <>
+
+ {completed
+ ? `${values.address} is now the Transfer Proxy for ${name} and can accept the transfer by logging into Bridge themselves. Until they accept your transfer, you will still have ownership over ${name}.`
+ : `Transfer ${name} to a new owner.`}
+
+
+ {!completed && (
+
+ )}
+
+
- pop()}
- />
+ pop()}
+ />
+ >
+ )}
+
);
}
diff --git a/src/views/Admin/Reticket/ReticketVerify.js b/src/views/Admin/Reticket/ReticketVerify.js
index 5f0b8fc3b..bd40089d9 100644
--- a/src/views/Admin/Reticket/ReticketVerify.js
+++ b/src/views/Admin/Reticket/ReticketVerify.js
@@ -1,12 +1,15 @@
import React, { useCallback, useMemo } from 'react';
-import { Input, Text, Grid } from 'indigo-react';
+import { Text, Grid } from 'indigo-react';
-import { useTicketInput } from 'lib/useInputs';
import { useLocalRouter } from 'lib/LocalRouter';
import { validateExactly } from 'lib/validators';
import { isDevelopment } from 'lib/flags';
-import { ForwardButton } from 'components/Buttons';
+import BridgeForm from 'form/BridgeForm';
+import { TicketInput } from 'form/Inputs';
+import { composeValidator, buildPatqValidator } from 'form/validators';
+import SubmitButton from 'form/SubmitButton';
+import FormError from 'form/FormError';
const STUB_VERIFY_TICKET = isDevelopment;
@@ -14,17 +17,22 @@ export default function ReticketVerify({ newWallet }) {
const { push, names } = useLocalRouter();
const ticket = newWallet.value.wallet.ticket;
- const validators = useMemo(
- () => [validateExactly(ticket, 'Does not match expected master ticket.')],
+ const validate = useMemo(
+ () =>
+ composeValidator({
+ ticket: buildPatqValidator([
+ validateExactly(ticket, 'Does not match expected master ticket.'),
+ ]),
+ }),
+ [ticket]
+ );
+
+ const initialValues = useMemo(
+ () => ({
+ ticket: STUB_VERIFY_TICKET ? ticket : 'undefined',
+ }),
[ticket]
);
- const [ticketInput, { pass }] = useTicketInput({
- name: 'ticket',
- label: 'New master ticket',
- initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
- autoFocus: true,
- validators,
- });
const goExecute = useCallback(() => push(names.EXECUTE), [push, names]);
@@ -33,15 +41,27 @@ export default function ReticketVerify({ newWallet }) {
Verify New Master Ticket
-
-
- Verify & Reticket
-
+
+ {({ handleSubmit }) => (
+ <>
+
+
+
+
+
+ Verify & Reticket
+
+ >
+ )}
+
);
}
diff --git a/src/views/CreateGalaxy.js b/src/views/CreateGalaxy.js
index a9e733c52..7efed2b0a 100644
--- a/src/views/CreateGalaxy.js
+++ b/src/views/CreateGalaxy.js
@@ -1,14 +1,12 @@
-import React, { useCallback, useEffect, useState } from 'react';
-import { Nothing, Just } from 'folktale/maybe';
+import React, { useCallback, useMemo, useState } from 'react';
import cn from 'classnames';
-import { Grid, Text, Input } from 'indigo-react';
+import { Grid, Text } from 'indigo-react';
import * as azimuth from 'azimuth-js';
import { useNetwork } from 'store/network';
import { usePointCache } from 'store/pointCache';
import * as need from 'lib/need';
-import { useAddressInput, useGalaxyInput } from 'lib/useInputs';
import useEthereumTransaction from 'lib/useEthereumTransaction';
import { GAS_LIMITS } from 'lib/constants';
import patp2dec from 'lib/patp2dec';
@@ -18,6 +16,15 @@ import { useLocalRouter } from 'lib/LocalRouter';
import ViewHeader from 'components/ViewHeader';
import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
import View from 'components/View';
+import BridgeForm from 'form/BridgeForm';
+import { PointInput, AddressInput } from 'form/Inputs';
+import {
+ composeValidator,
+ buildPointValidator,
+ buildAddressValidator,
+ hasErrors,
+} from 'form/validators';
+import FormError from 'form/FormError';
function useCreateGalaxy() {
const { contracts } = useNetwork();
@@ -45,9 +52,6 @@ export default function CreateGalaxy() {
const { contracts } = useNetwork();
const _contracts = need.contracts(contracts);
- const [error, setError] = useState();
- const [isAvailable, setIsAvailable] = useState(Nothing());
-
const {
construct,
unconstruct,
@@ -56,75 +60,54 @@ export default function CreateGalaxy() {
bind,
} = useCreateGalaxy();
- const [
- galaxyNameInput,
- { pass: validGalaxyName, syncPass: syncValidGalaxyName, value: galaxyName },
- // ^ we use value: here so our effect runs onChange
- ] = useGalaxyInput({
- name: 'galaxy',
- disabled: inputsLocked,
- autoFocus: true,
- error:
- error ||
- isAvailable.matchWith({
- Nothing: () => 'Loading availability...', // TODO: make async loading?
- Just: p => (p.value ? undefined : 'This galaxy is already owned.'),
- }),
- });
-
- const [ownerInput, { pass: validOwner, data: owner }] = useAddressInput({
- name: 'owner',
- label: `Ethereum Address`,
- disabled: inputsLocked,
- });
-
- useEffect(() => {
- if (validGalaxyName && validOwner) {
- construct(patp2dec(galaxyName), owner);
- } else {
- unconstruct();
- }
- }, [owner, construct, unconstruct, validGalaxyName, validOwner, galaxyName]);
-
- useEffect(() => {
- if (!syncValidGalaxyName || inputsLocked) {
- return;
- }
-
- setError();
- setIsAvailable(Nothing());
-
- let cancelled = false;
-
- (async () => {
- try {
- const currentOwner = await azimuth.azimuth.getOwner(
- _contracts,
- patp2dec(galaxyName)
- );
-
- const isAvailable = isZeroAddress(currentOwner);
-
- if (cancelled) {
- return;
- }
-
- setIsAvailable(Just(isAvailable));
- } catch (error) {
- console.error(error);
- setError(error.message);
- setIsAvailable(Just(false));
+ const validateFormAsync = useCallback(
+ async values => {
+ const currentOwner = await azimuth.azimuth.getOwner(
+ _contracts,
+ patp2dec(values.galaxyName)
+ );
+
+ const isAvailable = isZeroAddress(currentOwner);
+ if (!isAvailable) {
+ return { galaxyName: 'This galaxy is already spawned and owned.' };
+ }
+ },
+ [_contracts]
+ );
+
+ const validateForm = useCallback(
+ (values, errors) => {
+ if (hasErrors(errors)) {
+ return errors;
}
- })();
- return () => (cancelled = true);
- }, [
- _contracts,
- galaxyName,
- inputsLocked,
- setIsAvailable,
- syncValidGalaxyName,
- ]);
+ return validateFormAsync(values, errors);
+ },
+ [validateFormAsync]
+ );
+
+ const validate = useMemo(
+ () =>
+ composeValidator(
+ {
+ galaxyName: buildPointValidator(1),
+ owner: buildAddressValidator(),
+ },
+ validateForm
+ ),
+ [validateForm]
+ );
+
+ const onValues = useCallback(
+ ({ valid, values }) => {
+ if (valid) {
+ construct(patp2dec(values.galaxyName), values.owner);
+ } else {
+ unconstruct();
+ }
+ },
+ [construct, unconstruct]
+ );
return (
@@ -133,26 +116,49 @@ export default function CreateGalaxy() {
Create a Galaxy
- {completed && (
-
- {galaxyName} has been created and can be claimed by {owner}.
-
- )}
-
-
-
-
- pop()}
- />
+
+ {({ handleSubmit, values }) => (
+ <>
+ {completed && (
+
+ {values.galaxyName} has been created and can be claimed by{' '}
+ {values.owner}.
+
+ )}
+
+
+
+
+
+
+ pop()}
+ />
+ >
+ )}
+
);
diff --git a/src/views/Disclaimer.js b/src/views/Disclaimer.js
index 9d8e8f033..2dffd9c73 100644
--- a/src/views/Disclaimer.js
+++ b/src/views/Disclaimer.js
@@ -1,24 +1,28 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
import cn from 'classnames';
import { Grid, H3, B, Text, CheckboxInput } from 'indigo-react';
-import { useCheckboxInput } from 'lib/useInputs';
import { useHistory } from 'store/history';
import useHasDisclaimed from 'lib/useHasDisclaimed';
import View from 'components/View';
-import { ForwardButton } from 'components/Buttons';
import WarningBox from 'components/WarningBox';
+import BridgeForm from 'form/BridgeForm';
+import SubmitButton from 'form/SubmitButton';
+import { composeValidator, buildCheckboxValidator } from 'form/validators';
const TEXT_STYLE = 'f5';
export default function ActivateDisclaimer() {
const { pop } = useHistory();
const [, setHasDisclaimed] = useHasDisclaimed();
- const [understoodInput, { data: isUnderstood }] = useCheckboxInput({
- name: 'checkbox',
- label: 'I acknowledge and understand these rights',
- });
+
+ const validate = useMemo(
+ () => composeValidator({ checkbox: buildCheckboxValidator(true) }),
+ []
+ );
+
+ const initialValues = useMemo(() => ({ checkbox: false }), []);
const goBack = useCallback(async () => {
setHasDisclaimed(true);
@@ -76,15 +80,25 @@ export default function ActivateDisclaimer() {
Warning: Nobody but you can restore or reset your Master Ticket
-
-
- Continue
-
+
+ {({ handleSubmit }) => (
+ <>
+
+
+
+ Continue
+
+ >
+ )}
+
);
diff --git a/src/views/Invite/InviteEmail.js b/src/views/Invite/InviteEmail.js
index bbad83b6b..8bf7712b5 100644
--- a/src/views/Invite/InviteEmail.js
+++ b/src/views/Invite/InviteEmail.js
@@ -1,20 +1,25 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, {
+ useCallback,
+ useEffect,
+ useRef,
+ useMemo,
+ useState,
+} from 'react';
import cn from 'classnames';
import * as ob from 'urbit-ob';
import * as azimuth from 'azimuth-js';
import {
Grid,
Flex,
- Input,
IconButton,
HelpText,
Text,
ErrorText,
AccessoryIcon,
- useForm,
} from 'indigo-react';
import { uniq } from 'lodash';
import { fromWei, toWei } from 'web3-utils';
+import { FieldArray } from 'react-final-form-arrays';
import { usePointCursor } from 'store/pointCursor';
import { useNetwork } from 'store/network';
@@ -29,10 +34,8 @@ import {
hexify,
} from 'lib/txn';
import * as tank from 'lib/tank';
-import { useSuggestedGasPrice } from 'lib/useSuggestedGasPrice';
-import useArray from 'lib/useArray';
-import { buildEmailInputConfig } from 'lib/useInputs';
import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
+import { useSuggestedGasPrice } from 'lib/useSuggestedGasPrice';
import * as need from 'lib/need';
import * as wg from 'lib/walletgen';
import useSetState from 'lib/useSetState';
@@ -41,6 +44,21 @@ import useMailer from 'lib/useMailer';
import LoadableButton from 'components/LoadableButton';
import Highlighted from 'components/Highlighted';
+import BridgeForm from 'form/BridgeForm';
+import { Field } from 'react-final-form';
+import { EmailInput } from 'form/Inputs';
+import {
+ buildEmailValidator,
+ composeValidator,
+ hasErrors,
+ buildArrayValidator,
+} from 'form/validators';
+import { FORM_ERROR } from 'final-form';
+import SubmitButton from 'form/SubmitButton';
+import FormError from 'form/FormError';
+import { WARNING, onlyHasWarning } from 'form/helpers';
+
+const INITIAL_VALUES = { emails: [''] };
const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
@@ -55,6 +73,8 @@ const STATUS = {
FAILURE: 'FAILURE',
};
+const nameForEmailField = i => `emails[${i}]`;
+
const buttonText = (status, count) => {
switch (status) {
case STATUS.INPUT:
@@ -73,47 +93,29 @@ const buttonText = (status, count) => {
}
};
-// world's simplest uid
-let id = 0;
-const buildInputConfig = (extra = {}) =>
- buildEmailInputConfig({
- name: `email-${id++}`,
- placeholder: 'Email Address',
- ...extra,
- });
-
const buildAccessoryFor = (dones, errors) => name => {
if (dones[name]) return ;
if (errors[name]) return ;
return ;
};
-// TODO: test with tank, successful txs
export default function InviteEmail() {
- // TODO: resumption after error?
const { contracts, web3, networkType } = useNetwork();
const { wallet, walletType, walletHdPath } = useWallet();
const { syncInvites, getInvites } = usePointCache();
const { pointCursor } = usePointCursor();
const point = need.point(pointCursor);
- const { getHasReceived, syncHasReceivedForEmail, sendMail } = useMailer();
+ const { getHasReceived, sendMail } = useMailer();
const { gasPrice } = useSuggestedGasPrice(networkType);
+ const cachedEmails = useRef({});
+
const { availableInvites } = getInvites(point);
const maxInvitesToSend = availableInvites.matchWith({
Nothing: () => 0,
Just: p => p.value,
});
- // manage the array of input configs
- const [
- inputConfigs,
- { append: appendInput, removeAt: removeInputAt },
- ] = useArray(
- [buildInputConfig({ label: 'Email Address', autoFocus: true })],
- buildInputConfig
- );
-
// manage per-input state
const [hovered, setHovered] = useSetState();
const [invites, addInvite, clearInvites] = useSetState();
@@ -133,30 +135,38 @@ export default function InviteEmail() {
const isFailed = status === STATUS.FAILURE;
const isDone = status === STATUS.SUCCESS;
- // add disabled, error info to input configs
- const dynamicConfigs = useMemo(
- () =>
- inputConfigs.map(config => {
- config.disabled = !canInput;
- const hasReceivedError = getHasReceived(config.name).matchWith({
- Nothing: () => null, // loading
- Just: p => p.value && HAS_RECEIVED_TEXT,
- });
- config.error = hasReceivedError || errors[config.name];
- return config;
- }),
- [inputConfigs, errors, canInput, getHasReceived]
+ const validateHasReceived = useCallback(
+ async email => {
+ const hasReceived = await getHasReceived(email);
+ if (hasReceived) {
+ return HAS_RECEIVED_TEXT;
+ }
+ },
+ [getHasReceived]
);
- // construct the state of the set of inputs we're rendering below
- const { inputs, pass } = useForm(dynamicConfigs);
- // did all of the inputs pass inspection (and there are no general errors)?
- const allPass = pass && !generalError;
- // the form is submittable iff passing and input is allowed
- const canGenerate = allPass && canInput;
- // additional inputs can be added iff input is allowed and we have enough
- // invites to send
- const canAddInvite = canInput && inputConfigs.length < maxInvitesToSend;
+ const validateForm = useCallback((values, errors) => {
+ if (hasErrors(errors)) {
+ return errors;
+ }
+
+ // check for email uniqenesss
+ const emails = values.emails.filter(d => !!d);
+ if (uniq(emails).length !== emails.length) {
+ return { [FORM_ERROR]: 'Duplicate email.' };
+ }
+ }, []);
+
+ const validate = useMemo(
+ () =>
+ composeValidator(
+ {
+ emails: buildArrayValidator(buildEmailValidator(validateHasReceived)),
+ },
+ validateForm
+ ),
+ [validateForm, validateHasReceived]
+ );
// progress is [0, .length] of invites or receipts, as we're generating them
const progress = isGenerating
@@ -174,128 +184,113 @@ export default function InviteEmail() {
return () => null;
})();
- const generateInvites = useCallback(async () => {
- const _contracts = contracts.getOrElse(null);
- const _web3 = web3.getOrElse(null);
- const _wallet = wallet.getOrElse(null);
- if (!_contracts || !_web3 || !_wallet) {
- // not using need because we want a custom error
- throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
- }
-
- //TODO want to do this on-input, but that gets weird. see #188
- let knowAll = true;
- let alreadyReceived = [];
- await Promise.all(
- inputs.map(async input => {
- const email = input.data;
- getHasReceived(email).matchWith({
- Nothing: () => {
- knowAll = false;
- },
- Just: p => {
- if (p.value) alreadyReceived.push(email);
- },
- });
- })
- );
- if (!knowAll) {
- throw new Error('No word yet from email service...');
- }
- if (alreadyReceived.length > 0) {
- throw new Error(
- 'The following recipients already own a point: ' +
- alreadyReceived.join(', ')
- );
- }
-
- const nonce = await _web3.eth.getTransactionCount(_wallet.address);
- const chainId = await _web3.eth.net.getId();
- const planets = await azimuth.delegatedSending.getPlanetsToSend(
- _contracts,
- point,
- inputs.length
- );
-
- // account for the race condition where invites got used up while we were
- // composing our target list
- if (planets.length < inputs.length) {
- // resync invites to the cache, since they're out of date
- syncInvites(point);
+ const generateInvites = useCallback(
+ async values => {
+ const _contracts = contracts.getOrElse(null);
+ const _web3 = web3.getOrElse(null);
+ const _wallet = wallet.getOrElse(null);
+ if (!_contracts || !_web3 || !_wallet) {
+ // not using need because we want a custom error
+ throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
+ }
- throw new Error(
- `Can currently only send ${planets.length} invites. ` +
- `Please remove invites until you are within the limit.`
+ const nonce = await _web3.eth.getTransactionCount(_wallet.address);
+ const chainId = await _web3.eth.net.getId();
+ const planets = await azimuth.delegatedSending.getPlanetsToSend(
+ _contracts,
+ point,
+ values.emails.length
);
- }
- clearInvites();
- // NB(shrugs) - must be processed in serial because main thread, etc
- let errorCount = 0;
- for (let i = 0; i < inputs.length; i++) {
- const input = inputs[i];
- try {
- const { data: email, name } = inputs[i];
-
- const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
- MIN_PLANET
- // we're always giving planets, so generate a ticket of the correct size
- );
+ // account for the race condition where invites got used up while we were
+ // composing our target list
+ if (planets.length < values.emails.length) {
+ // resync invites to the cache, since they're out of date
+ syncInvites(point);
+
+ return {
+ [FORM_ERROR]:
+ `Can currently only send ${planets.length} invites. ` +
+ `Please remove invites until you are within the limit.`,
+ };
+ }
- const inviteTx = azimuth.delegatedSending.sendPoint(
- _contracts,
- point,
- planets[i],
- owner.keys.address
- );
- const signedTx = await signTransaction({
- wallet: _wallet,
- walletType,
- walletHdPath,
- networkType,
- // TODO: ^ make a useTransactionSigner to encapsulate this logic
- txn: inviteTx,
- gasPrice: gasPrice.toString(),
- gasLimit: GAS_LIMIT.toString(),
- nonce: nonce + i,
- chainId,
- });
- const rawTx = hexify(signedTx.serialize());
-
- addInvite({ [name]: { email, ticket, signedTx, rawTx } });
- } catch (error) {
- console.error(error);
- errorCount++;
- addError({ [input.name]: `Wallet Error: ${input.email}` });
+ clearInvites();
+ // NB(shrugs) - must be processed in serial because main thread, etc
+ let errorCount = 0;
+ for (let i = 0; i < values.emails.length; i++) {
+ const name = nameForEmailField(i);
+ try {
+ const email = values.emails[i];
+ const planet = planets[i];
+
+ const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
+ MIN_PLANET
+ // we're always giving planets, so generate a ticket of the correct size
+ );
+
+ const inviteTx = azimuth.delegatedSending.sendPoint(
+ _contracts,
+ point,
+ planet,
+ owner.keys.address
+ );
+
+ const signedTx = await signTransaction({
+ wallet: _wallet,
+ walletType,
+ walletHdPath,
+ networkType,
+ chainId,
+ nonce: nonce + i,
+ // TODO: ^ make a useTransactionSigner to encapsulate this logic
+ txn: inviteTx,
+ gasPrice: gasPrice.toString(),
+ gasLimit: GAS_LIMIT.toString(),
+ });
+
+ const rawTx = hexify(signedTx.serialize());
+
+ addInvite({ [name]: { email, ticket, signedTx, rawTx } });
+ } catch (error) {
+ console.error(error);
+ errorCount++;
+ addError({ [name]: 'Error generating invite.' });
+ }
}
- }
- if (errorCount > 0) {
- throw new Error(
- `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
- errorCount,
- 'error'
- )} while generating wallets.`
- );
- }
- }, [
- contracts,
- web3,
- gasPrice,
- wallet,
- inputs,
- point,
- clearInvites,
- getHasReceived,
- addError,
- syncInvites,
- walletType,
- walletHdPath,
- networkType,
- addInvite,
- ]);
+ if (errorCount > 0) {
+ return {
+ [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
+ errorCount,
+ 'error'
+ )} while generating wallets. You can still send the invites that generated correctly.`,
+ };
+ }
+ },
+ [
+ contracts,
+ web3,
+ wallet,
+ point,
+ clearInvites,
+ syncInvites,
+ walletType,
+ walletHdPath,
+ networkType,
+ gasPrice,
+ addInvite,
+ addError,
+ ]
+ );
const sendInvites = useCallback(async () => {
+ // compute all of the names of the fields
+ const allFieldNames = Object.keys(cachedEmails.current);
+ // only emails we're sending are the valid ones with invites
+ const names = allFieldNames.filter(name => !!invites[name]);
+ const emails = allFieldNames.map(name => cachedEmails.current[name]);
+
const _web3 = web3.getOrElse(null);
const _wallet = wallet.getOrElse(null);
if (!_web3 || !_wallet) {
@@ -307,8 +302,8 @@ export default function InviteEmail() {
_web3,
point,
_wallet.address,
- toWei((gasPrice * GAS_LIMIT * inputs.length).toString(), 'gwei'),
- inputs.map(input => invites[input.name].rawTx),
+ toWei((gasPrice * GAS_LIMIT * emails.length).toString(), 'gwei'),
+ names.map(name => invites[name].rawTx),
(address, minBalance, balance) =>
setNeedFunds({ address, minBalance, balance }),
() => setNeedFunds(undefined)
@@ -319,8 +314,8 @@ export default function InviteEmail() {
let unsentInvites = [];
let orphanedInvites = [];
- const txAndMailings = inputs.map(async input => {
- const invite = invites[input.name];
+ const txAndMailings = names.map(async name => {
+ const invite = invites[name];
try {
const txHash = await sendSignedTransaction(
_web3,
@@ -328,9 +323,7 @@ export default function InviteEmail() {
tankWasUsed
);
- // TODO: waitForTransactionConfirm never rejects
- const didConfirm = await waitForTransactionConfirm(_web3, txHash);
- if (!didConfirm) throw new Error();
+ await waitForTransactionConfirm(_web3, txHash);
} catch (error) {
console.error(error);
unsentInvites.push(invite);
@@ -338,19 +331,18 @@ export default function InviteEmail() {
}
try {
- const success = await sendMail(
+ await sendMail(
invite.email,
invite.ticket,
ob.patp(point),
invite.rawTx
);
- if (!success) throw new Error('Failed to send mail');
} catch (error) {
console.error(error);
orphanedInvites.push(invite);
}
- addReceipt({ [input.name]: true });
+ addReceipt({ [name]: true });
});
await Promise.all(txAndMailings);
@@ -376,46 +368,48 @@ export default function InviteEmail() {
}
}, [
web3,
+ wallet,
+ point,
gasPrice,
- inputs,
- addReceipt,
- clearReceipts,
invites,
- point,
- wallet,
+ clearReceipts,
+ addReceipt,
sendMail,
]);
- const onClick = useCallback(async () => {
+ const onSubmit = useCallback(
+ async values => {
+ cachedEmails.current = values.emails.reduce((memo, email, i) => {
+ memo[nameForEmailField(i)] = email;
+ return memo;
+ }, {});
+ setStatus(STATUS.GENERATING);
+ const errors = await generateInvites(values);
+ if (hasErrors(errors)) {
+ if (onlyHasWarning(errors)) {
+ setStatus(STATUS.CAN_SEND);
+ }
+
+ return errors;
+ }
+
+ setStatus(STATUS.CAN_SEND);
+ },
+ [generateInvites]
+ );
+
+ const doSend = useCallback(async () => {
setGeneralError(null);
try {
- if (canGenerate) {
- setStatus(STATUS.GENERATING);
- await generateInvites();
- setStatus(STATUS.CAN_SEND);
- } else if (canSend) {
- setStatus(STATUS.SENDING);
- await sendInvites();
- setStatus(STATUS.SUCCESS);
- }
+ setStatus(STATUS.SENDING);
+ await sendInvites();
+ setStatus(STATUS.SUCCESS);
} catch (error) {
console.error(error);
- setGeneralError(error);
+ setGeneralError(error.message);
setStatus(STATUS.FAILURE);
}
- }, [canGenerate, canSend, generateInvites, sendInvites]);
-
- // when inputs update, check to see if any of them are duplicates
- useEffect(() => {
- // compute the list of valid emails
- const emails = inputs.map(i => i.data).filter(d => !!d);
- if (uniq(emails).length !== emails.length) {
- setGeneralError(new Error(`Duplicate email.`));
- } else {
- setGeneralError(null);
- setStatus(STATUS.INPUT);
- }
- }, [inputs]);
+ }, [sendInvites]);
// when we transition to done, sync invites because we just sent some
// and therefore know that state has changed
@@ -425,109 +419,142 @@ export default function InviteEmail() {
}
}, [isDone, syncInvites, point]);
- useEffect(() => {
- for (const input of inputs) {
- if (input.pass) {
- syncHasReceivedForEmail(input.data);
- }
- }
- }, [inputs, syncHasReceivedForEmail]);
-
return (
-
- {/* use hidden class instead of removing component from dom */}
- {/* in order to avoid janky reflow */}
- appendInput()}
- disabled={!canAddInvite}
- className={cn({ hidden: isDone })}
- solid>
- +
-
-
-
- {isDone && (
- <>
-
- {pluralize(inputs.length, 'invite')}{' '}
- {pluralize(inputs.length, 'has', 'have')} been successfully sent
-
- {inputs.map(input => (
-
- {invites[input.name].email}
-
- ))}
- >
- )}
-
- {!isDone && (
- <>
- {/* email inputs */}
- {inputs.map((input, i) => {
- const isFirst = i === 0;
- return (
- setHovered({ [input.name]: true })}
- onMouseLeave={() => setHovered({ [input.name]: false })}
- full>
-
- {!isFirst &&
- (input.focused || hovered[input.name]) &&
- canInput && (
-
- removeInputAt(i)}
- solid
- secondary>
- -
-
-
+
+ {({ handleSubmit }) => (
+
+ {({ fields }) => {
+ const sentInviteNames = Object.keys(receipts);
+ return (
+ <>
+
+ {/* use hidden class instead of removing component from dom */}
+ {/* in order to avoid janky reflow */}
+ fields.push('')}
+ disabled={!canInput || fields.length >= maxInvitesToSend}
+ className={cn({ hidden: isDone })}
+ solid>
+ +
+
+
+
+ {isDone ? (
+ <>
+
+
+ {pluralize(sentInviteNames.length, 'invite')}
+ {' '}
+ {pluralize(sentInviteNames.length, 'has', 'have')} been
+ successfully sent
+
+
+ {sentInviteNames.map(name => (
+
+
+ {({ input: { value } }) => value}
+
+
+ ))}
+ >
+ ) : (
+ <>
+ {fields.map((name, i) => {
+ const isFirst = i === 0;
+ return (
+ setHovered({ [name]: true })}
+ onMouseLeave={() => setHovered({ [name]: false })}>
+
+
+ {({ meta: { active } }) => {
+ return (
+ <>
+ {!isFirst &&
+ (active || hovered[name]) &&
+ canInput && (
+
+ fields.remove(i)}
+ solid
+ secondary>
+ -
+
+
+ )}
+ >
+ );
+ }}
+
+
+ );
+ })}
+
+ {canInput ? (
+
+ {buttonText(status, fields.length)}
+
+ ) : (
+
+ {buttonText(status, fields.length)}
+
+ )}
+
+ {needFunds && (
+
+
+ Your ownership address {needFunds.address} needs at
+ least {fromWei(needFunds.minBalance)} ETH and
+ currently has {fromWei(needFunds.balance)} ETH.
+ Waiting until the account has enough funds.
+
+
+ )}
+
+
+
+ {generalError && (
+
+ {generalError}
+
+ )}
+ >
)}
-
- );
- })}
-
-
- {buttonText(status, inputs.length)}
-
-
- {needFunds && (
-
-
- Your ownership address {needFunds.address} needs at least{' '}
- {fromWei(needFunds.minBalance)} ETH and currently has{' '}
- {fromWei(needFunds.balance)} ETH. Waiting until the account has
- enough funds.
-
-
- )}
-
- {generalError && (
-
- {generalError.message.toString()}
-
- )}
- >
- )}
+ >
+ );
+ }}
+
+ )}
+
);
}
diff --git a/src/views/IssueChild.js b/src/views/IssueChild.js
index 1fda805cf..48ebe6c24 100644
--- a/src/views/IssueChild.js
+++ b/src/views/IssueChild.js
@@ -1,7 +1,6 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { Nothing, Just } from 'folktale/maybe';
+import React, { useCallback, useMemo, useState } from 'react';
import cn from 'classnames';
-import { Grid, Text, Input } from 'indigo-react';
+import { Grid, Text } from 'indigo-react';
import * as azimuth from 'azimuth-js';
import * as ob from 'urbit-ob';
@@ -10,18 +9,25 @@ import { usePointCache } from 'store/pointCache';
import { usePointCursor } from 'store/pointCursor';
import * as need from 'lib/need';
-import { useAddressInput, usePointInput } from 'lib/useInputs';
import useEthereumTransaction from 'lib/useEthereumTransaction';
import { GAS_LIMITS } from 'lib/constants';
import patp2dec from 'lib/patp2dec';
-import useLifecycle from 'lib/useLifecycle';
-import { validateNameInNumberSet } from 'lib/validators';
import { getSpawnCandidate } from 'lib/child';
import { useLocalRouter } from 'lib/LocalRouter';
+import useConstant from 'lib/useConstant';
import ViewHeader from 'components/ViewHeader';
import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
import View from 'components/View';
+import { PointInput, AddressInput } from 'form/Inputs';
+import {
+ composeValidator,
+ buildPointValidator,
+ buildAddressValidator,
+ hasErrors,
+} from 'form/validators';
+import BridgeForm from 'form/BridgeForm';
+import FormError from 'form/FormError';
function useIssueChild() {
const { contracts } = useNetwork();
@@ -55,7 +61,11 @@ export default function IssueChild() {
const _contracts = need.contracts(contracts);
const _point = parseInt(need.point(pointCursor), 10);
- const [availablePoints, setAvailablePoints] = useState(Nothing());
+ const availablePointsPromise = useConstant(() =>
+ azimuth.azimuth
+ .getUnspawnedChildren(_contracts, _point)
+ .then(points => new Set(points))
+ );
const candidates = useMemo(() => {
const getCandidate = () => ob.patp(getSpawnCandidate(_point));
@@ -72,62 +82,51 @@ export default function IssueChild() {
bind,
} = useIssueChild();
- const validators = useMemo(
- () => [
- validateNameInNumberSet(
- availablePoints.getOrElse(new Set()),
- 'This point cannot be spawned.'
- ),
- ],
- [availablePoints]
+ const validateFormAsync = useCallback(
+ async values => {
+ const point = patp2dec(values.name);
+ const hasPoint = (await availablePointsPromise).has(point);
+
+ if (!hasPoint) {
+ return { point: 'This point cannot be spawned.' };
+ }
+ },
+ [availablePointsPromise]
);
- const [
- pointNameInput,
- { pass: validPointName, value: pointName },
- // ^ we use value: here so our effect runs onChange
- ] = usePointInput({
- name: 'point',
- disabled: inputsLocked,
- autoFocus: true,
- validators,
- error: availablePoints.matchWith({
- Nothing: () => 'Loading availability...',
- Just: () => undefined,
- }),
- });
-
- const [ownerInput, { pass: validOwner, data: owner }] = useAddressInput({
- name: 'owner',
- label: `Ethereum Address`,
- disabled: inputsLocked,
- });
-
- useEffect(() => {
- if (validPointName && validOwner) {
- construct(patp2dec(pointName), owner);
- } else {
- unconstruct();
- }
- }, [owner, construct, unconstruct, validPointName, validOwner, pointName]);
-
- useLifecycle(() => {
- let mounted = true;
-
- (async () => {
- const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
- _contracts,
- _point
- );
-
- if (!mounted) {
- return;
+
+ const validateForm = useCallback(
+ (values, errors) => {
+ if (hasErrors(errors)) {
+ return errors;
}
- setAvailablePoints(Just(new Set(availablePoints)));
- })();
+ return validateFormAsync(values, errors);
+ },
+ [validateFormAsync]
+ );
+
+ const validate = useMemo(
+ () =>
+ composeValidator(
+ {
+ point: buildPointValidator(4),
+ owner: buildAddressValidator(),
+ },
+ validateForm
+ ),
+ [validateForm]
+ );
- return () => (mounted = false);
- });
+ const onValues = useCallback(
+ ({ valid, values }) => {
+ if (valid) {
+ construct(patp2dec(values.point), values.owner);
+ } else {
+ unconstruct();
+ }
+ },
+ [construct, unconstruct]
+ );
return (
@@ -143,30 +142,52 @@ export default function IssueChild() {
)}
- {completed && (
-
- {pointName} has been spawned and can be claimed by {owner}.
-
- )}
-
- {!completed && (
- <>
-
-
- >
- )}
-
- pop()}
- />
+
+ {({ handleSubmit, values }) => (
+ <>
+ {completed && (
+
+ {values.point} has been spawned and can be claimed by{' '}
+ {values.owner}.
+
+ )}
+
+ {!completed && (
+ <>
+
+
+ >
+ )}
+
+
+
+ pop()}
+ />
+ >
+ )}
+
);
diff --git a/src/views/Login.js b/src/views/Login.js
index 2a392f365..89ccd627d 100644
--- a/src/views/Login.js
+++ b/src/views/Login.js
@@ -1,5 +1,4 @@
import React, { useCallback, useState } from 'react';
-import { Nothing } from 'folktale/maybe';
import { H4, Grid } from 'indigo-react';
import { useHistory } from 'store/history';
@@ -60,7 +59,7 @@ const walletTypeToViewName = walletType => {
export default function Login() {
// globals
const { pop, push, names } = useHistory();
- const { wallet, walletType } = useWallet();
+ const { walletType } = useWallet();
// inputs
const [currentTab, setCurrentTab] = useState(
@@ -72,7 +71,7 @@ export default function Login() {
names.ACTIVATE,
]);
- const doContinue = useCallback(() => {
+ const goHome = useCallback(() => {
push(names.POINTS);
}, [push, names]);
@@ -88,21 +87,14 @@ export default function Login() {
full
as={Tabs}
className="mt1"
+ // Tabs
views={VIEWS}
options={OPTIONS}
currentTab={currentTab}
onTabChange={setCurrentTab}
+ // Tab extra
+ goHome={goHome}
/>
-
-
- Continue
-