on master help sign in
Everything but the invite code
51e0c25Will Galebach
src/assets/metamask.pngnew file

Binary file not shown.

src/assets/metamask.svgnew file+9
@@ -0,0 +1,9 @@
1+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2+<rect width="24" height="24" fill="url(#pattern0)"/>
3+<defs>
4+<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
5+<use xlink:href="#image0" transform="scale(0.00444444)"/>
6+</pattern>
7+<image id="image0" width="225" height="225" xlink:href="data:image/png;base64,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"/>
8+</defs>
9+</svg>
src/assets/wallet-connect.pngnew file

Binary file not shown.

src/assets/wallet-connect.svgnew file+3
@@ -0,0 +1,3 @@
1+<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M4.9133 3.51903C8.82718 -0.312985 15.1728 -0.312985 19.0867 3.51903L19.5578 3.98022C19.7535 4.17182 19.7535 4.48247 19.5578 4.67407L17.9464 6.25171C17.8486 6.34751 17.6899 6.34751 17.5921 6.25171L16.9439 5.61706C14.2135 2.94375 9.78657 2.94375 7.05614 5.61706L6.36196 6.29672C6.26412 6.39252 6.10547 6.39252 6.00763 6.29672L4.39629 4.71908C4.20059 4.52748 4.20059 4.21683 4.39629 4.02523L4.9133 3.51903ZM22.4191 6.78176L23.8532 8.18586C24.0489 8.37746 24.0489 8.68811 23.8532 8.87971L17.3868 15.211C17.1911 15.4026 16.8738 15.4026 16.6781 15.211C16.6781 15.211 16.6781 15.211 16.6781 15.211L12.0886 10.7175C12.0397 10.6696 11.9604 10.6696 11.9115 10.7175C11.9115 10.7175 11.9115 10.7175 11.9115 10.7175L7.32207 15.211C7.12638 15.4026 6.80909 15.4026 6.6134 15.211C6.6134 15.211 6.61339 15.211 6.61339 15.211L0.146771 8.87963C-0.0489235 8.68803 -0.0489235 8.37738 0.146771 8.18578L1.58087 6.78168C1.77656 6.59008 2.09385 6.59008 2.28954 6.78168L6.8791 11.2752C6.92802 11.3231 7.00734 11.3231 7.05626 11.2752C7.05626 11.2752 7.05626 11.2752 7.05627 11.2752L11.6456 6.78168C11.8413 6.59007 12.1586 6.59006 12.3543 6.78166C12.3543 6.78166 12.3543 6.78167 12.3543 6.78167L16.9438 11.2752C16.9927 11.3231 17.0721 11.3231 17.121 11.2752L21.7105 6.78176C21.9062 6.59016 22.2234 6.59016 22.4191 6.78176Z" fill="#3B99FC"/>
3+</svg>
src/components/L2/Dropdowns/FeeDropdown.scss+15
@@ -19,12 +19,27 @@
19 justify-content: space-between;
19 justify-content: space-between;
20 align-items: center;
20 align-items: center;
21 cursor: pointer;
21 cursor: pointer;
22+ line-height: 20px;
22
23
23 padding: 8px 12px 8px 16px;
24 padding: 8px 12px 8px 16px;
24
25
25 &:hover {
26 &:hover {
26 opacity: 0.8;
27 opacity: 0.8;
27 }
28 }
29+
30+ .label {
31+ margin-right: 32px;
32+ }
33+
34+ .custom-input {
35+ font-family: "Inter", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji";
36+ text-align: right;
37+ height: 32px;
38+ }
39+
40+ .unit {
41+ margin-left: 4px;
42+ }
28 }
43 }
29 }
44 }
30 }
45 }
src/components/L2/Dropdowns/FeeDropdown.tsx+48−11
@@ -1,5 +1,5 @@
1−import React, { useCallback, useEffect, useState } from 'react';
1+import React, { useCallback, useEffect, useState, FormEvent } from 'react';
2−import { Box, Row } from '@tlon/indigo-react';
2+import { Box, Row, StatelessTextInput } from '@tlon/indigo-react';
3import { DEFAULT_GAS_PRICE_GWEI } from 'lib/constants';
3import { DEFAULT_GAS_PRICE_GWEI } from 'lib/constants';
4import useGasPrice from 'lib/useGasPrice';
4import useGasPrice from 'lib/useGasPrice';
5
5
@@ -10,20 +10,49 @@ const PRICE_LABELS = ['Fast', 'Average', 'Slow'];
10
10
11export interface GasPrice {
11export interface GasPrice {
12 price: number;
12 price: number;
13− wait: number;
13+ wait: string;
14}
14}
15
15
16−const formatPriceWait = ({ price, wait }: GasPrice) =>
17− `${price} gwei (${Math.round(wait * 100) / 100} min)`;
16+export const formatWait = (wait: number) => Math.round(wait * 100) / 100;
17+export const formatDisplay = ({ price, wait }: GasPrice) =>
18+ `${price} gwei (${wait} min)`;
18
19
20+export default function FeeDropdown({
21+ setGasPrice,
22+}: {
19−const FeeDropdown = ({ setGasPrice }: { setGasPrice: (g: number) => void }) => {
23+ setGasPrice: (g: number) => void;
24+}) {
20 const { suggestedGasPrices } = useGasPrice(DEFAULT_GAS_PRICE_GWEI);
25 const { suggestedGasPrices } = useGasPrice(DEFAULT_GAS_PRICE_GWEI);
21
26
22− const [open, setOpen] = useState<boolean>(false);
27+ const [open, setOpen] = useState(false);
28+ const [custom, setCustom] = useState<string>('0');
23 const [selected, setSelected] = useState<GasPrice>(
29 const [selected, setSelected] = useState<GasPrice>(
24 suggestedGasPrices.average
30 suggestedGasPrices.average
25 );
31 );
26
32
33+ const handleCustom = (e: FormEvent) => {
34+ const cleanedValue = e.target.value.replace(/[^0-9]/g, '');
35+ const cleanedNum = Number(cleanedValue);
36+ setCustom(cleanedValue);
37+ setGasPrice(cleanedNum);
38+
39+ let customWait = 'Unknown';
40+
41+ const { fast, average, low } = suggestedGasPrices;
42+
43+ if (cleanedNum < low.price) {
44+ customWait = `> ${low.wait}`;
45+ } else if (cleanedNum < average.price) {
46+ customWait = `~${low.wait}`;
47+ } else if (cleanedNum < fast.price) {
48+ customWait = `~${average.wait}`;
49+ } else {
50+ customWait = `< ${fast.wait}`;
51+ }
52+
53+ setSelected({ price: cleanedValue, wait: customWait });
54+ };
55+
27 useEffect(() => {
56 useEffect(() => {
28 setSelected(suggestedGasPrices.average);
57 setSelected(suggestedGasPrices.average);
29 }, [suggestedGasPrices]);
58 }, [suggestedGasPrices]);
@@ -41,7 +70,7 @@ const FeeDropdown = ({ setGasPrice }: { setGasPrice: (g: number) => void }) => {
41 <Dropdown
70 <Dropdown
42 className="fee-dropdown"
71 className="fee-dropdown"
43 open={open}
72 open={open}
44− value={formatPriceWait(selected)}
73+ value={formatDisplay(selected)}
45 toggleOpen={() => setOpen(!open)}>
74 toggleOpen={() => setOpen(!open)}>
46 <Box className="prices">
75 <Box className="prices">
47 {Object.values(suggestedGasPrices).map(
76 {Object.values(suggestedGasPrices).map(
@@ -50,13 +79,21 @@ const FeeDropdown = ({ setGasPrice }: { setGasPrice: (g: number) => void }) => {
50 className="price"
79 className="price"
51 onClick={selectPrice(value)}
80 onClick={selectPrice(value)}
52 key={value.wait}>
81 key={value.wait}>
53− {PRICE_LABELS[ind]}: {formatPriceWait(value)}
82+ {PRICE_LABELS[ind]}: {formatDisplay(value)}
54 </Row>
83 </Row>
55 )
84 )
56 )}
85 )}
86+ <Row className="price">
87+ <Box className="label">Custom:</Box>
88+ <StatelessTextInput
89+ value={custom}
90+ className="custom-input"
91+ placeholder="0"
92+ onChange={handleCustom}
93+ />
94+ <Box className="unit">gwei</Box>
95+ </Row>
57 </Box>
96 </Box>
58 </Dropdown>
97 </Dropdown>
59 );
98 );
60};
99};
61−
62−export default FeeDropdown;
src/components/L2/Headers/L2BackButton.scssnew file+10
@@ -0,0 +1,10 @@
1+@import 'style/shared/variables.scss';
2+
3+svg.back-button {
4+ padding: 6px 9px;
5+ width: 14px;
6+ height: 20px;
7+ background-color: $washed-gray;
8+ border-radius: $medium-border-radius;
9+ cursor: pointer;
10+}
src/components/L2/Headers/L2BackButton.tsxnew file+21
@@ -0,0 +1,21 @@
1+import React from 'react';
2+import { Icon } from '@tlon/indigo-react';
3+import './L2BackButton.scss';
4+
5+interface L2BackButtonProps {
6+ onBack?: () => void;
7+ className?: string;
8+}
9+
10+export default function L2BackButton({
11+ onBack,
12+ className = '',
13+}: L2BackButtonProps) {
14+ return (
15+ <Icon
16+ className={`back-button ${className}`}
17+ icon="ChevronWest"
18+ onClick={onBack}
19+ />
20+ );
21+}
src/components/L2/Headers/L2BackHeader.scss+2−7
@@ -8,13 +8,8 @@
8 font-size: 14px;
8 font-size: 14px;
9 margin: 48px 0px 8px;
9 margin: 48px 0px 8px;
10
10
11− svg.back-button {
11+ .login-margins {
12− padding: 6px 9px;
12+ margin: 0px 0px 16px;
13− width: 14px;
14− height: 20px;
15− background-color: $washed-gray;
16− border-radius: $medium-border-radius;
17− cursor: pointer;
18 }
13 }
19
14
20 .rollup-timer {
15 .rollup-timer {
src/components/L2/Headers/L2BackHeader.tsx+9−3
@@ -12,13 +12,19 @@ import { toBN } from 'web3-utils';
12import BN from 'bn.js';
12import BN from 'bn.js';
13import { isDevelopment } from 'lib/flags';
13import { isDevelopment } from 'lib/flags';
14import { ReactComponent as Wallet } from 'assets/wallet.svg';
14import { ReactComponent as Wallet } from 'assets/wallet.svg';
15+import L2BackButton from './L2BackButton';
15
16
16export interface L2BackHeaderProps {
17export interface L2BackHeaderProps {
17 back?: () => void;
18 back?: () => void;
18 hideBalance?: boolean;
19 hideBalance?: boolean;
20+ className: string;
19}
21}
20
22
21−const L2BackHeader = ({ back, hideBalance = false }: L2BackHeaderProps) => {
23+const L2BackHeader = ({
24+ back,
25+ className = '',
26+ hideBalance = false,
27+}: L2BackHeaderProps) => {
22 const { config } = useRoller();
28 const { config } = useRoller();
23 const { nextRoll, currentL2 } = useRollerStore(store => store);
29 const { nextRoll, currentL2 } = useRollerStore(store => store);
24 const { pop }: any = useHistory();
30 const { pop }: any = useHistory();
@@ -62,8 +68,8 @@ const L2BackHeader = ({ back, hideBalance = false }: L2BackHeaderProps) => {
62 const hasBalance = Number(ethBalance.toString()) !== 0;
68 const hasBalance = Number(ethBalance.toString()) !== 0;
63
69
64 return (
70 return (
65− <Row className="l2-back-header">
71+ <Row className={`l2-back-header ${className}`}>
66− <Icon className="back-button" icon="ChevronWest" onClick={goBack} />
72+ <L2BackButton onBack={goBack} />
67 {currentL2 && !hideBalance ? (
73 {currentL2 && !hideBalance ? (
68 <Row className="rollup-timer">
74 <Row className="rollup-timer">
69 <Icon icon="Clock" />
75 <Icon icon="Clock" />
src/components/L2/Modal.scss+9
@@ -11,4 +11,13 @@
11 flex-direction: column;
11 flex-direction: column;
12 justify-content: center;
12 justify-content: center;
13 align-items: center;
13 align-items: center;
14+
15+ .close {
16+ position: absolute;
17+ top: 3px;
18+ right: 8px;
19+ font-size: 18px;
20+ font-weight: 200;
21+ cursor: pointer;
22+ }
14}
23}
src/components/L2/Window/BodyPane.tsx+3−2
@@ -5,10 +5,11 @@ import './BodyPane.scss';
5
5
6export interface BodyPaneProps {
6export interface BodyPaneProps {
7 children: React.ReactNode;
7 children: React.ReactNode;
8+ className?: string;
8}
9}
9
10
10−const BodyPane = ({ children }: BodyPaneProps) => {
11+const BodyPane = ({ children, className = '' }: BodyPaneProps) => {
11− return <Box className="body-pane">{children}</Box>;
12+ return <Box className={`body-pane ${className}`}>{children}</Box>;
12};
13};
13
14
14export default BodyPane;
15export default BodyPane;
src/components/L2/Window/HeaderPane.scss+5
@@ -19,6 +19,11 @@
19 justify-content: space-between;
19 justify-content: space-between;
20 align-items: center;
20 align-items: center;
21
21
22+ svg {
23+ height: 16px;
24+ width: 16px;
25+ }
26+
22 .header-button {
27 .header-button {
23 background-color: black;
28 background-color: black;
24 color: white;
29 color: white;
src/indigo-react/components/Button.js+2−2
@@ -34,9 +34,9 @@ export default function Button({
34 tabIndex={!disabled ? 0 : undefined}
34 tabIndex={!disabled ? 0 : undefined}
35 onKeyPress={handleKeyPress}
35 onKeyPress={handleKeyPress}
36 className={cn(
36 className={cn(
37− 'rel pointer pv4 truncate flex-row justify-between us-none',
37+ 'rel pointer pv2 truncate flex-row justify-between us-none r4',
38 {
38 {
39− p4: solid,
39+ p2: solid,
40 },
40 },
41 {
41 {
42 'bg-green3': success && !disabled,
42 'bg-green3': success && !disabled,
src/indigo-react/components/Input.js+1−1
@@ -86,7 +86,7 @@ export default function Input({
86 // NOTE: 1.15 from input line-height, 24px = 12px * 2 (from p3 styling)
86 // NOTE: 1.15 from input line-height, 24px = 12px * 2 (from p3 styling)
87 style={type === 'textarea' ? { minHeight: 'calc(1.5rem * 3)' } : {}}
87 style={type === 'textarea' ? { minHeight: 'calc(1.5rem * 3)' } : {}}
88 className={cn(
88 className={cn(
89− 'b b1 p3 outline-none bs-none',
89+ 'b b1 p3 outline-none bs-none r4',
90 { mono },
90 { mono },
91 {
91 {
92 'bg-white': !disabled,
92 'bg-white': !disabled,
src/lib/getSuggestedGasPrice.js+7−6
@@ -1,5 +1,6 @@
1import { NETWORK_TYPES } from './network';
1import { NETWORK_TYPES } from './network';
2import { DEFAULT_GAS_PRICE_GWEI, MAX_GAS_PRICE_GWEI } from './constants';
2import { DEFAULT_GAS_PRICE_GWEI, MAX_GAS_PRICE_GWEI } from './constants';
3+import { formatWait } from 'components/L2/Dropdowns/FeeDropdown';
3
4
4// ethgasstation returns values in floating point, one order of magitude
5// ethgasstation returns values in floating point, one order of magitude
5// more than gwei. see: https://docs.ethgasstation.info
6// more than gwei. see: https://docs.ethgasstation.info
@@ -9,15 +10,15 @@ const minGas = gas => Math.min(Math.ceil(gas / 10), MAX_GAS_PRICE_GWEI);
9export const defaultGasValues = value => ({
10export const defaultGasValues = value => ({
10 fast: {
11 fast: {
11 price: value,
12 price: value,
12− wait: 1,
13+ wait: '1',
13 },
14 },
14 average: {
15 average: {
15 price: value,
16 price: value,
16− wait: 1,
17+ wait: '1',
17 },
18 },
18 low: {
19 low: {
19 price: value,
20 price: value,
20− wait: 1,
21+ wait: '1',
21 },
22 },
22});
23});
23
24
@@ -46,15 +47,15 @@ export default async function getSuggestedGasPrice(networkType) {
46 return {
47 return {
47 fast: {
48 fast: {
48 price: minGas(json.fast),
49 price: minGas(json.fast),
49− wait: json.fastWait,
50+ wait: formatWait(json.fastWait),
50 },
51 },
51 average: {
52 average: {
52 price: minGas(json.average),
53 price: minGas(json.average),
53− wait: json.avgWait,
54+ wait: formatWait(json.avgWait),
54 },
55 },
55 low: {
56 low: {
56 price: minGas(json.safeLow),
57 price: minGas(json.safeLow),
57− wait: json.safeLowWait,
58+ wait: formatWait(json.safeLowWait),
58 },
59 },
59 };
60 };
60 } catch (e) {
61 } catch (e) {
src/lib/router.js+1−1
@@ -1,6 +1,6 @@
1import Disclaimer from 'views/Disclaimer';
1import Disclaimer from 'views/Disclaimer';
2import Login from 'views/Login';
2import Login from 'views/Login';
3−import Other from 'views/Login/Other.js';
3+import Other from 'views/Login/Other';
4import Senate from 'views/Point/Senate';
4import Senate from 'views/Point/Senate';
5import Documents from 'views/Senate/Documents';
5import Documents from 'views/Senate/Documents';
6import Upgrades from 'views/Senate/Upgrades';
6import Upgrades from 'views/Senate/Upgrades';
src/lib/utils/invite.ts+5−2
@@ -10,8 +10,11 @@ export const generateUrl = (ticket: string, planet: number) => {
10 return 'No Ticket or Ship';
10 return 'No Ticket or Ship';
11};
11};
12
12
13−export const generateCsvLine = (ticket: string, planet: number) =>
13+export const generateCsvLine = (ind: number, ticket: string, planet: number) =>
14− `${ob.patp(planet)},${generateUrl(ticket, planet)}\n`;
14+ `${ind + 1},${ob.patp(planet)},${generateUrl(
15+ ticket,
16+ planet
17+ )},${planet},${ticket}\n`;
15
18
16export const generateCsvName = (defaultName: string, point: number) =>
19export const generateCsvName = (defaultName: string, point: number) =>
17 `${ob.patp(point).slice(1)}_${defaultName}`;
20 `${ob.patp(point).slice(1)}_${defaultName}`;
src/style/shared/variables.scss+2
@@ -11,3 +11,5 @@ $dark-gray: rgba(102, 102, 102, 1);
11$rollup-yellow: rgba(255, 199, 0, 1);
11$rollup-yellow: rgba(255, 199, 0, 1);
12$accept-green: rgba(0, 159, 101, 1);
12$accept-green: rgba(0, 159, 101, 1);
13$warning-red: rgb(242, 33, 33);
13$warning-red: rgb(242, 33, 33);
14+$light-gray-border: 1px solid $light-gray;
15+$activate-blue: rgba(33, 157, 255, 1);
src/views/Invite/Cohort.tsx+3−3
@@ -203,9 +203,9 @@ export default function InviteCohort() {
203 const invites = await getInvites(currentL2, page, true);
203 const invites = await getInvites(currentL2, page, true);
204 if (invites) {
204 if (invites) {
205 const csv = invites.reduce(
205 const csv = invites.reduce(
206− (csvData, { ticket, planet }) =>
206+ (csvData, { ticket, planet }, ind) =>
207− (csvData += generateCsvLine(ticket, planet)),
207+ (csvData += generateCsvLine(ind, ticket, planet)),
208− 'Planet,Invite URL\n'
208+ 'Number,Planet,Invite URL,Point,Ticket\n'
209 );
209 );
210 const hiddenElement = document.createElement('a');
210 const hiddenElement = document.createElement('a');
211 hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
211 hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(
src/views/Login.tsx+7−46
@@ -1,5 +1,5 @@
1import React, { useCallback, useState } from 'react';
1import React, { useCallback, useState } from 'react';
2−import { Grid, Text, Button, Flex, LinkButton } from 'indigo-react';
2+import { Grid, Text, Flex } from 'indigo-react';
3
3
4import { versionLabel } from 'lib/flags';
4import { versionLabel } from 'lib/flags';
5
5
@@ -10,22 +10,16 @@ import { COMMANDS, useFlowCommand } from 'lib/flowCommand';
10import View from 'components/View';
10import View from 'components/View';
11import Footer from 'components/Footer';
11import Footer from 'components/Footer';
12
12
13−import Ticket from './Login/Ticket';
14import Other from './Login/Other';
13import Other from './Login/Other';
15
14
16export default function Login() {
15export default function Login() {
17 // globals
16 // globals
18− const { push, names } = useHistory();
17+ const { push, names }: any = useHistory();
19− const flow = useFlowCommand();
18+ const flow: any = useFlowCommand();
20
19
21 // inputs
20 // inputs
22 const [isOther, setisOther] = useState(false);
21 const [isOther, setisOther] = useState(false);
23
22
24− const goToActivate = useCallback(() => push(names.ACTIVATE), [
25− push,
26− names.ACTIVATE,
27− ]);
28−
29 const goHome = useCallback(() => {
23 const goHome = useCallback(() => {
30 if (!flow) {
24 if (!flow) {
31 push(names.POINTS);
25 push(names.POINTS);
@@ -70,50 +64,17 @@ export default function Login() {
70 <View inset>
64 <View inset>
71 <Grid>
65 <Grid>
72 <Grid.Item full as={Text} className="flex justify-center mt9 mb7">
66 <Grid.Item full as={Text} className="flex justify-center mt9 mb7">
73− <Grid.Item as={Text} className="gray3">
67+ <Grid.Item as={Text}>Bridge /&nbsp;</Grid.Item>
74− Urbit ID /&nbsp;
68+ <Grid.Item className="fw-bold" as={Text}>
69+ Log In
75 </Grid.Item>
70 </Grid.Item>
76− <Grid.Item as={Text}>Login</Grid.Item>
77 </Grid.Item>
71 </Grid.Item>
78 {flow && (
72 {flow && (
79 <Grid.Item full as={Text} className="t-center mb4">
73 <Grid.Item full as={Text} className="t-center mb4">
80 {flowDescription(flow)}
74 {flowDescription(flow)}
81 </Grid.Item>
75 </Grid.Item>
82 )}
76 )}
83− <Grid.Item full as={isOther ? Other : Ticket} goHome={goHome} />
77+ <Grid.Item full as={Other} goHome={goHome} className="login-selector" />
84− {!isOther && (
85− <>
86− <Grid.Item full className="t-center mv4 gray4">
87− or
88− </Grid.Item>
89− <Grid.Item
90− full
91− as={Button}
92− className="b-solid b1 b-black"
93− center
94− onClick={() => setisOther(true)}>
95− Metamask, Mnemonic, Hardware Wallet...
96− </Grid.Item>
97− {!flow && (
98− <Grid.Item
99− full
100− onClick={goToActivate}
101− className="mv10 t-center f6">
102− <span className="gray4">New Urbit ID? </span>
103− <LinkButton>Activate</LinkButton>
104− </Grid.Item>
105− )}
106− </>
107− )}
108− {isOther && (
109− <Grid.Item
110− as={LinkButton}
111− onClick={() => setisOther(false)}
112− full
113− className="t-center underline f6 mt8">
114− Back
115− </Grid.Item>
116− )}
117 </Grid>
78 </Grid>
118
79
119 <Footer>
80 <Footer>
src/views/Login/LoginSelector.scssnew file+78
@@ -0,0 +1,78 @@
1+@import 'style/shared/variables.scss';
2+
3+.login-selector {
4+ width: 100%;
5+ min-height: 300px;
6+
7+ .login-menu-item {
8+ padding: 16px 16px;
9+ font-size: 14px;
10+ font-weight: 600;
11+ line-height: 24px;
12+ border-radius: $medium-border-radius;
13+ border: $light-gray-border;
14+ cursor: pointer;
15+ margin-top: 12px;
16+ display: flex;
17+ justify-content: space-between;
18+ align-items: center;
19+
20+ &:first-of-type {
21+ margin-top: 48px;
22+ }
23+
24+ svg {
25+ height: 18px;
26+ width: 16px;
27+ }
28+
29+ svg.logo-icon {
30+ height: 24px;
31+ width: 24px;
32+ }
33+ }
34+
35+ button.activate {
36+ font-size: 14px;
37+ color: white;
38+ font-weight: 600;
39+ background-color: $activate-blue;
40+ margin-top: 12px;
41+ height: 56px;
42+ border: none;
43+ border-radius: $medium-border-radius;
44+ font-family: $default-font;
45+ }
46+
47+ .unsupported-info {
48+ font-size: 14px;
49+ line-height: 18px;
50+ margin-top: 16px;
51+ font-family: $default-font;
52+ text-align: center;
53+ cursor: pointer;
54+
55+ svg {
56+ margin-bottom: -3px;
57+ margin-right: 4px;
58+ height: 16px;
59+ width: 16px;
60+ }
61+ }
62+
63+ .info-modal-content {
64+ position: relative;
65+ background-color: white;
66+ height: 302px;
67+ width: 320px;
68+ border-radius: $large-border-radius;
69+ font-family: $default-font;
70+ padding: 16px 48px 16px 16px;
71+ font-size: 14px;
72+ line-height: 24px;
73+
74+ display: flex;
75+ flex-direction: column;
76+ justify-content: space-between;
77+ }
78+}
src/views/Login/LoginSelector.tsxnew file+117
@@ -0,0 +1,117 @@
1+import React, { useCallback, useState } from 'react';
2+import cn from 'classnames';
3+import { Grid } from 'indigo-react';
4+import Window from 'components/L2/Window/Window';
5+import HeaderPane from 'components/L2/Window/HeaderPane';
6+import BodyPane from 'components/L2/Window/BodyPane';
7+import { Box, Button, Icon, Row, Text } from '@tlon/indigo-react';
8+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
9+
10+import './LoginSelector.scss';
11+import { useHistory } from 'store/history';
12+import Modal from 'components/L2/Modal';
13+import L2BackButton from 'components/L2/Headers/L2BackButton';
14+
15+interface LoginSelectorProps {
16+ className: string;
17+ views: { [key: string]: React.ReactNode };
18+ options: {
19+ text: string;
20+ value: string;
21+ menuIcon: React.ReactNode;
22+ headerIcon: React.ReactNode;
23+ }[];
24+ currentTab?: string;
25+ setCurrentTab: (tab?: string) => void;
26+}
27+
28+export default function LoginSelector({
29+ className,
30+ views,
31+ options,
32+ currentTab,
33+ setCurrentTab,
34+ // Tab props
35+ ...rest
36+}: LoginSelectorProps) {
37+ const { push, names }: any = useHistory();
38+ const [showModal, setShowModal] = useState(false);
39+
40+ const goToActivate = useCallback(() => push(names.ACTIVATE), [
41+ push,
42+ names.ACTIVATE,
43+ ]);
44+
45+ if (!currentTab) {
46+ return (
47+ <Grid className={className}>
48+ {options.map((option, i) => (
49+ <React.Fragment key={option.value}>
50+ <Grid.Item
51+ full
52+ className={cn('f5 pv3 rel pointer login-menu-item')}
53+ onClick={() => setCurrentTab(option.value)}>
54+ {option.text}
55+ {option.menuIcon}
56+ </Grid.Item>
57+ </React.Fragment>
58+ ))}
59+ <Grid.Item full className="activate" as={Button} onClick={goToActivate}>
60+ Activate a Planet
61+ </Grid.Item>
62+ <Grid.Item
63+ full
64+ className="unsupported-info"
65+ as={Text}
66+ onClick={() => setShowModal(true)}>
67+ <Icon icon="Info" />I have a wallet type not supported here
68+ </Grid.Item>
69+ <Modal show={showModal} hide={() => setShowModal(false)}>
70+ <Box className="info-modal-content">
71+ <Box className="close" onClick={() => setShowModal(false)}>
72+ &#215;
73+ </Box>
74+ <span>
75+ <span className="fw-bold">Other Wallet Types</span> <br />
76+ All other wallet types are now supported via Metamask or
77+ WalletConnect.
78+ <br />
79+ <br />
80+ If you are you using a{' '}
81+ <span className="fw-bold">
82+ Hardware Wallet, Ethereum Keystore, Ethereum Private Key
83+ </span>{' '}
84+ please use Metamask going forward.
85+ <br />
86+ <br />
87+ If you are using a{' '}
88+ <span className="fw-bold">non-custodial wallet</span> or{' '}
89+ <span className="fw-bold">mobile wallet</span>, please use
90+ WalletConnect going forward.
91+ </span>
92+ </Box>
93+ </Modal>
94+ </Grid>
95+ );
96+ }
97+
98+ const Tab = views[currentTab];
99+ const currentOption = options.find(({ value }) => value === currentTab);
100+
101+ return (
102+ <Box display="flex" flexDirection="column">
103+ <L2BackButton className="mb4" onBack={() => setCurrentTab(undefined)} />
104+ <Window>
105+ <HeaderPane>
106+ <Row className="header-row">
107+ <h5>{currentOption?.text}</h5>
108+ {currentOption?.headerIcon}
109+ </Row>
110+ </HeaderPane>
111+ <BodyPane className="login-body-pane">
112+ <Grid.Item full as={Tab} {...rest} className="login-selector" />
113+ </BodyPane>
114+ </Window>
115+ </Box>
116+ );
117+}
src/views/Login/Metamask.js+10−4
@@ -74,10 +74,16 @@ function Login({ onSubmit, goHome }) {
74 </Grid.Item>
74 </Grid.Item>
75 <BridgeForm onSubmit={onSubmit} afterSubmit={goHome}>
75 <BridgeForm onSubmit={onSubmit} afterSubmit={goHome}>
76 {({ handleSubmit }) => (
76 {({ handleSubmit }) => (
77+ <Grid.Item full className="flex-col justify-end">
78+ <Grid.Item
79+ center
80+ full
81+ as={SubmitButton}
77− <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
82+ handleSubmit={handleSubmit}>
78− Login
83+ Login
79− {window.ethereum.selectedAddress &&
84+ {window.ethereum.selectedAddress &&
80− ' as ' + abbreviateAddress(window.ethereum.selectedAddress)}
85+ ' as ' + abbreviateAddress(window.ethereum.selectedAddress)}
86+ </Grid.Item>
81 </Grid.Item>
87 </Grid.Item>
82 )}
88 )}
83 </BridgeForm>
89 </BridgeForm>
src/views/Login/Mnemonic.js+36−30
@@ -71,52 +71,58 @@ export default function Mnemonic({ className, goHome }) {
71 };
71 };
72
72
73 return (
73 return (
74− <Grid className={cn('mt4', className)}>
74+ <Grid className={cn(className)}>
75 <BridgeForm
75 <BridgeForm
76 validate={validate}
76 validate={validate}
77 onValues={onValues}
77 onValues={onValues}
78 afterSubmit={goHome}
78 afterSubmit={goHome}
79 initialValues={initialValues}>
79 initialValues={initialValues}>
80 {({ handleSubmit }) => (
80 {({ handleSubmit }) => (
81− <>
81+ <Grid.Item full className="flex-col justify-between">
82− <Grid.Item
82+ <Grid.Item full>
83− full
84− as={MnemonicInput}
83+ <Grid.Item full as={MnemonicInput} name="mnemonic" />
85− name="mnemonic"
86− label="BIP39 Mnemonic"
87− />
88
84
89− <Grid.Item
85+ <Grid.Item
90− full
86+ full
91− as={CheckboxInput}
87+ as={CheckboxInput}
92− name="anyMnemonic"
88+ name="anyMnemonic"
93− label="Skip mnemonic validation"
89+ label="Skip passphrase validation"
90+ />
91+
92+ <Condition when="useAdvanced" is={true}>
93+ <Grid.Item
94+ full
95+ as={PassphraseInput}
96+ name="passphrase"
97+ label="Passphrase"
94− />
98+ />
99+
100+ <Grid.Item
101+ full
102+ as={HdPathInput}
103+ name="hdpath"
104+ label="HD Path"
105+ />
106+ </Condition>
95
107
96− <Condition when="useAdvanced" is={true}>
97 <Grid.Item
108 <Grid.Item
98 full
109 full
99− as={PassphraseInput}
110+ as={CheckboxInput}
100− name="passphrase"
111+ name="useAdvanced"
101− label="Passphrase"
112+ label="Passphrase & HD Path"
102 />
113 />
103
114
104− <Grid.Item full as={HdPathInput} name="hdpath" label="HD Path" />
115+ <Grid.Item full as={FormError} />
105− </Condition>
116+ </Grid.Item>
106
117
107 <Grid.Item
118 <Grid.Item
108 full
119 full
109− as={CheckboxInput}
120+ as={SubmitButton}
110− name="useAdvanced"
111− label="Passphrase & HD Path"
112− />
113−
114− <Grid.Item full as={FormError} />
115−
116− <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
121+ handleSubmit={handleSubmit}
117− Login
122+ center>
123+ Log In
118 </Grid.Item>
124 </Grid.Item>
119− </>
125+ </Grid.Item>
120 )}
126 )}
121 </BridgeForm>
127 </BridgeForm>
122 </Grid>
128 </Grid>
src/views/Login/Other.jsdeleted−75
@@ -1,75 +0,0 @@
1−import React, { useState, useMemo } from 'react';
2−import { Grid } from 'indigo-react';
3−
4−import useBreakpoints from 'lib/useBreakpoints';
5−
6−import Accordion from 'components/Accordion';
7−
8−import PrivateKey from './PrivateKey';
9−import Keystore from './Keystore';
10−import Ledger from './Ledger';
11−import Metamask from './Metamask';
12−import Trezor from './Trezor';
13−import Mnemonic from './Mnemonic';
14−import WalletConnect from './WalletConnect';
15−
16−const NAMES = {
17− PRIVATE_KEY: 'PRIVATE_KEY',
18− KEYSTORE: 'KEYSTORE',
19− LEDGER: 'LEDGER',
20− TREZOR: 'TREZOR',
21− METAMASK: 'METAMASK',
22− MNEMONIC: 'MNEMONIC',
23− WALLET_CONNECT: 'WALLET_CONNECT',
24−};
25−
26−const VIEWS = {
27− [NAMES.PRIVATE_KEY]: PrivateKey,
28− [NAMES.KEYSTORE]: Keystore,
29− [NAMES.LEDGER]: Ledger,
30− [NAMES.TREZOR]: Trezor,
31− [NAMES.METAMASK]: Metamask,
32− [NAMES.MNEMONIC]: Mnemonic,
33− [NAMES.WALLET_CONNECT]: WalletConnect,
34−};
35−
36−const OPTIONS = [
37− { text: 'Metamask', value: NAMES.METAMASK },
38− { text: 'Mnemonic (BIP 39)', value: NAMES.MNEMONIC },
39− { text: 'Ledger', value: NAMES.LEDGER },
40− { text: 'Trezor', value: NAMES.TREZOR },
41− { text: 'Ethereum Private Key', value: NAMES.PRIVATE_KEY },
42− { text: 'Ethereum Keystore', value: NAMES.KEYSTORE },
43− { text: 'WalletConnect', value: NAMES.WALLET_CONNECT },
44−];
45−
46−const isHardware = name => !![NAMES.TREZOR, NAMES.LEDGER].find(n => n === name);
47−
48−export default function Other({ className, ...rest }) {
49− const [currentTab, setCurrentTab] = useState(undefined);
50−
51− const hardwareAllowed = useBreakpoints([false, true, true]);
52− const options = useMemo(
53− () =>
54− OPTIONS.map(o =>
55− !isHardware(o.value)
56− ? o
57− : { ...o, disabled: !hardwareAllowed && ' not available on mobile' }
58− ),
59− [hardwareAllowed]
60− );
61− return (
62− <>
63− <Grid.Divider />
64− <Accordion
65− className={className}
66− views={VIEWS}
67− options={options}
68− currentTab={currentTab}
69− onTabChange={setCurrentTab}
70− //
71− {...rest}
72− />
73− </>
74− );
75−}
src/views/Login/Other.tsxnew file+77
@@ -0,0 +1,77 @@
1+import React, { useState } from 'react';
2+import { Icon } from '@tlon/indigo-react';
3+
4+import Metamask from './Metamask';
5+import Ticket from './Ticket';
6+import Mnemonic from './Mnemonic';
7+import WalletConnect from './WalletConnect';
8+import LoginSelector from './LoginSelector';
9+import { ReactComponent as MetamaskIcon } from 'assets/metamask.svg';
10+import { ReactComponent as WalletConnectIcon } from 'assets/wallet-connect.svg';
11+
12+const NAMES = {
13+ METAMASK: 'METAMASK',
14+ MNEMONIC: 'MNEMONIC',
15+ WALLET_CONNECT: 'WALLET_CONNECT',
16+ MASTER_TICKET: 'MASTER_TICKET',
17+};
18+
19+const VIEWS = {
20+ [NAMES.METAMASK]: Metamask,
21+ [NAMES.MNEMONIC]: Mnemonic,
22+ [NAMES.WALLET_CONNECT]: WalletConnect,
23+ [NAMES.MASTER_TICKET]: Ticket,
24+};
25+
26+const rightArrowIcon = <Icon icon="ChevronEast" />;
27+const settingsIcon = <Icon icon="Gear" />;
28+
29+const OPTIONS = [
30+ {
31+ text: 'Master Key',
32+ value: NAMES.MASTER_TICKET,
33+ menuIcon: rightArrowIcon,
34+ headerIcon: settingsIcon,
35+ },
36+ {
37+ text: 'Metamask',
38+ value: NAMES.METAMASK,
39+ menuIcon: <MetamaskIcon className="logo-icon" />,
40+ headerIcon: settingsIcon,
41+ },
42+ {
43+ text: 'WalletConnect',
44+ value: NAMES.WALLET_CONNECT,
45+ menuIcon: <WalletConnectIcon className="logo-icon" />,
46+ headerIcon: settingsIcon,
47+ },
48+ {
49+ text: 'Recovery Phrase',
50+ value: NAMES.MNEMONIC,
51+ menuIcon: rightArrowIcon,
52+ headerIcon: settingsIcon,
53+ },
54+];
55+
56+interface OtherProps {
57+ className: string;
58+ goHome: () => void;
59+}
60+
61+export default function Other({ className, ...rest }: OtherProps) {
62+ const [currentTab, setCurrentTab] = useState<string | undefined>(undefined);
63+
64+ return (
65+ <>
66+ <LoginSelector
67+ className={className}
68+ views={VIEWS}
69+ options={OPTIONS}
70+ currentTab={currentTab}
71+ setCurrentTab={setCurrentTab}
72+ //
73+ {...rest}
74+ />
75+ </>
76+ );
77+}
src/views/Login/Ticket.js+59−56
@@ -209,66 +209,69 @@ export default function Ticket({ className, goHome }) {
209 validate={validate}
209 validate={validate}
210 onSubmit={onSubmit}
210 onSubmit={onSubmit}
211 afterSubmit={goHome}
211 afterSubmit={goHome}
212+ className="flex-col justify-between"
212 initialValues={initialValues}>
213 initialValues={initialValues}>
213 {({ handleSubmit, values, submitting }) => (
214 {({ handleSubmit, values, submitting }) => (
214− <>
215+ <Grid.Item full className="flex-col justify-between">
216+ <Grid.Item full>
215− <Grid.Item full as={PointInput} name="point" />
217+ <Grid.Item full as={PointInput} name="point" />
216
218
217− <Condition when="useShards" is={false}>
219+ <Condition when="useShards" is={false}>
218− <Grid.Item
220+ <Grid.Item
219− full
221+ full
220− as={TicketInput}
222+ as={TicketInput}
221− hidden={values.ticketHidden}
223+ hidden={values.ticketHidden}
222− labelAccessory={<AdvancedOptions />}
224+ labelAccessory={<AdvancedOptions />}
223− accessory={<TicketInputAccessory name="ticketHidden" />}
225+ accessory={<TicketInputAccessory name="ticketHidden" />}
224− className="mt3"
226+ className="mt3"
225− name="ticket"
227+ name="ticket"
226− label="Master Ticket"
228+ label="Master Ticket"
227− />
229+ />
228− </Condition>
230+ </Condition>
229
231
230− <Condition when="useShards" is={true}>
232+ <Condition when="useShards" is={true}>
231− <Grid.Item
233+ <Grid.Item
232− full
234+ full
233− as={TicketInput}
235+ as={TicketInput}
234− accessory={<TicketInputAccessory name="ticketHidden" />}
236+ accessory={<TicketInputAccessory name="ticketHidden" />}
235− hidden={values.ticketHidden}
237+ hidden={values.ticketHidden}
236− labelAccessory={<AdvancedOptions />}
238+ labelAccessory={<AdvancedOptions />}
237− className="mt3"
239+ className="mt3"
238− name="shard1"
240+ name="shard1"
239− label="Shard 1"
241+ label="Shard 1"
240− />
242+ />
241− <Grid.Item
243+ <Grid.Item
242− full
244+ full
243− as={TicketInput}
245+ as={TicketInput}
244− accessory={<TicketInputAccessory name="ticketHidden" />}
246+ accessory={<TicketInputAccessory name="ticketHidden" />}
245− hidden={values.ticketHidden}
247+ hidden={values.ticketHidden}
246− className="mt3"
248+ className="mt3"
247− name="shard2"
249+ name="shard2"
248− label="Shard 2"
250+ label="Shard 2"
249− />
251+ />
250− <Grid.Item
252+ <Grid.Item
251− full
253+ full
252− as={TicketInput}
254+ as={TicketInput}
253− className="mt3"
255+ className="mt3"
254− accessory={<TicketInputAccessory name="ticketHidden" />}
256+ accessory={<TicketInputAccessory name="ticketHidden" />}
255− hidden={values.ticketHidden}
257+ hidden={values.ticketHidden}
256− name="shard3"
258+ name="shard3"
257− label="Shard 3"
259+ label="Shard 3"
258− />
260+ />
259− </Condition>
261+ </Condition>
260
262
261− <Condition when="usePassphrase" is={true}>
263+ <Condition when="usePassphrase" is={true}>
262− <Grid.Item
264+ <Grid.Item
263− full
265+ full
264− as={PassphraseInput}
266+ as={PassphraseInput}
265− className="mt3"
267+ className="mt3"
266− name="passphrase"
268+ name="passphrase"
267− label="Wallet Passphrase"
269+ label="Wallet Passphrase"
268− />
270+ />
269− </Condition>
271+ </Condition>
270
272
271− <Grid.Item full as={FormError} />
273+ <Grid.Item full as={FormError} />
274+ </Grid.Item>
272
275
273 <Grid.Item
276 <Grid.Item
274 full
277 full
@@ -280,10 +283,10 @@ export default function Ticket({ className, goHome }) {
280 ? 'Logging in...'
283 ? 'Logging in...'
281 : isWarning
284 : isWarning
282 ? 'Login Anyway'
285 ? 'Login Anyway'
283− : 'Login'
286+ : 'Log In'
284 }
287 }
285 </Grid.Item>
288 </Grid.Item>
286− </>
289+ </Grid.Item>
287 )}
290 )}
288 </BridgeForm>
291 </BridgeForm>
289 </Grid>
292 </Grid>
src/views/Login/WalletConnect.tsx+62−62
@@ -30,71 +30,71 @@ const WalletConnectLogin = ({ className, goHome }) => {
30
30
31 return (
31 return (
32 connector && (
32 connector && (
33− <>
34− <Grid className={className}>
33+ <Grid className={className}>
35− {!isConnected() && (
34+ {!isConnected() && (
36− <Grid.Item full as={LegacyText} className="f6 gray4 mb3">
35+ <Grid.Item full as={LegacyText} className="f6 gray4 mb3">
37− Note that WalletConnect is a young protocol, not all wallets may
36+ Note that WalletConnect is a young protocol, not all wallets may
38− work fully
37+ work fully
39− </Grid.Item>
40− )}
41−
42− {isConnected() && peerMeta && (
43− <Grid.Item full className="f6 gray5 mt3 mb3">
44− <Grid>
45− <Grid.Item style={{ gridArea: 'auto / 3 / auto / 5' }}>
46− {peerIcon ? (
47− <Image src={peerIcon} />
48− ) : (
49− <LegacyText>Icon Unavailable</LegacyText>
50− )}
51− </Grid.Item>
38+ </Grid.Item>
52− <Grid.Item
53− className={'flex-col align-center justify-center'}
54− style={{ gridArea: 'auto / 6 / auto / 11' }}>
55− <LegacyText as="div">{peerMeta.name}</LegacyText>
56− <LegacyText as="div">
57− {address ? abbreviateAddress(address) : null}
58− </LegacyText>
59− <RestartButton
60− as="a"
61− className={'gray3'}
62− onClick={disconnect}>
63− disconnect
64− </RestartButton>
65− </Grid.Item>
66− </Grid>
67− </Grid.Item>
68− )}
39+ )}
69
40
70− <BridgeForm onSubmit={onSubmit} afterSubmit={goHome}>
41+ {isConnected() && peerMeta && (
71− {({ handleSubmit, submitting }) => (
42+ <Grid.Item full className="f6 gray5 mt3 mb3">
72− <>
43+ <Grid>
73− {isConnected() ? (
44+ <Grid.Item style={{ gridArea: 'auto / 3 / auto / 5' }}>
74− <>
45+ {peerIcon ? (
75− <Grid.Item full as={FormError} />
46+ <Image src={peerIcon} />
76−
77− <Grid.Item
78− full
79− as={SubmitButton}
80− handleSubmit={handleSubmit}>
81− {submitting
82− ? 'Please check your WalletConnect wallet'
83− : 'Authenticate'}
84− </Grid.Item>
85− </>
86 ) : (
47 ) : (
87− <>
48+ <LegacyText>Icon Unavailable</LegacyText>
88− <Grid.Item full as={ForwardButton} solid onClick={connect}>
89− {'Connect'}
90− </Grid.Item>
91− </>
92 )}
49 )}
93− </>
50+ </Grid.Item>
94− )}
51+ <Grid.Item
95− </BridgeForm>
52+ className={'flex-col align-center justify-center'}
53+ style={{ gridArea: 'auto / 6 / auto / 11' }}>
54+ <LegacyText as="div">{peerMeta.name}</LegacyText>
55+ <LegacyText as="div">
56+ {address ? abbreviateAddress(address) : null}
57+ </LegacyText>
58+ <RestartButton
59+ as="a"
60+ className={'gray3'}
61+ onClick={disconnect}>
62+ disconnect
63+ </RestartButton>
64+ </Grid.Item>
65+ </Grid>
66+ </Grid.Item>
67+ )}
68+
69+ <BridgeForm onSubmit={onSubmit} afterSubmit={goHome}>
70+ {({ handleSubmit, submitting }) => (
71+ <Grid.Item full className="flex-col justify-end">
72+ {isConnected() ? (
73+ <>
74+ <Grid.Item full as={FormError} />
75+
76+ <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
77+ {submitting
78+ ? 'Please check your WalletConnect wallet'
79+ : 'Authenticate'}
96− </Grid>
80+ </Grid.Item>
97− </>
81+ </>
82+ ) : (
83+ <>
84+ <Grid.Item
85+ full
86+ center
87+ as={ForwardButton}
88+ solid
89+ onClick={connect}>
90+ {'Connect'}
91+ </Grid.Item>
92+ </>
93+ )}
94+ </Grid.Item>
95+ )}
96+ </BridgeForm>
97+ </Grid>
98 )
98 )
99 );
99 );
100};
100};
src/views/Point/Point.scss−9
@@ -58,15 +58,6 @@
58 position: relative;
58 position: relative;
59 width: 368px;
59 width: 368px;
60
60
61− .close {
62− position: absolute;
63− top: 3px;
64− right: 8px;
65− font-size: 18px;
66− font-weight: 200;
67− cursor: pointer;
68− }
69−
70 .title {
61 .title {
71 font-weight: 700;
62 font-weight: 700;
72 margin-bottom: 8px;
63 margin-bottom: 8px;