Skip to content

Commit 8df1e6d

Browse files
committed
ui+connect: add QR code and magic links for sessions
1 parent 0009177 commit 8df1e6d

File tree

14 files changed

+342
-34
lines changed

14 files changed

+342
-34
lines changed

Diff for: app/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"mobx": "6.3.2",
3838
"mobx-react-lite": "3.2.0",
3939
"mobx-utils": "6.0.4",
40+
"qrcode.react": "^3.1.0",
41+
"rc-dialog": "^8.9.0",
4042
"rc-select": "11.5.0",
4143
"rc-tooltip": "4.2.1",
4244
"react": "17.0.2",

Diff for: app/src/App.scss

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
// react-component component styles
1818
@import '../node_modules/rc-tooltip/assets/bootstrap_white.css';
19+
@import '../node_modules/rc-dialog/assets/index.css';
1920
@import './assets/styles/rc-select.scss';
2021

2122
// react-toastify styles

Diff for: app/src/assets/icons/bolt-outlined.svg

+5
Loading

Diff for: app/src/assets/icons/qr.svg

+13
Loading

Diff for: app/src/components/base/icons.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { ReactComponent as RefreshIcon } from 'assets/icons/refresh-cw.svg';
3030
import { ReactComponent as SettingsIcon } from 'assets/icons/settings.svg';
3131
import { ReactComponent as CancelIcon } from 'assets/icons/slash.svg';
3232
import { ReactComponent as UserPlusIcon } from 'assets/icons/user-plus.svg';
33+
import { ReactComponent as QRCodeIcon } from 'assets/icons/qr.svg';
34+
import { ReactComponent as BoltOutlinedIcon } from 'assets/icons/bolt-outlined.svg';
3335

3436
interface IconProps {
3537
size?: 'x-small' | 'small' | 'medium' | 'large';
@@ -119,3 +121,5 @@ export const Settings = Icon.withComponent(SettingsIcon);
119121
export const UserPlus = Icon.withComponent(UserPlusIcon);
120122
export const BarChart = Icon.withComponent(BarChartIcon);
121123
export const List = Icon.withComponent(ListIcon);
124+
export const QRCode = Icon.withComponent(QRCodeIcon);
125+
export const BoltOutlined = Icon.withComponent(BoltOutlinedIcon);

Diff for: app/src/components/base/text.tsx

+46
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,49 @@ export const Jumbo = styled.span`
4848
font-size: ${props => props.theme.sizes.xl};
4949
line-height: 38px;
5050
`;
51+
52+
//
53+
// v2 Text Styles
54+
//
55+
56+
interface TextProps {
57+
bold?: boolean;
58+
semiBold?: boolean;
59+
center?: boolean;
60+
block?: boolean;
61+
muted?: boolean;
62+
space?: 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 96 | 120 | 160 | 200;
63+
desktopSpace?: 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 96 | 120 | 160 | 200;
64+
}
65+
66+
const BaseText = styled.span<TextProps>`
67+
// On larger devices, make bold elements bold instead of semi-bold
68+
font-family: ${props =>
69+
props.bold
70+
? props.theme.fonts.open.bold
71+
: props.semiBold
72+
? props.theme.fonts.open.semiBold
73+
: props.theme.fonts.open.regular};
74+
75+
// The text-align property is ignored on mobile
76+
${props => props.muted && `color: ${props.theme.colors.gray};`}
77+
${props => props.space && `margin-bottom: ${props.space}px;`}
78+
text-align: ${props => (props.center ? 'center' : 'left')};
79+
`;
80+
81+
const BaseBlock = BaseText.withComponent('div');
82+
83+
export const DisplayLarge = styled(BaseBlock)`
84+
font-size: 40px;
85+
line-height: 48px;
86+
`;
87+
88+
export const Display = styled(BaseBlock)`
89+
font-size: 32px;
90+
line-height: 40px;
91+
`;
92+
93+
export const Paragraph = styled(BaseBlock)`
94+
font-size: 16px;
95+
line-height: 24px;
96+
`;

Diff for: app/src/components/common/Modal.tsx

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from 'react';
2+
import { Global, Theme } from '@emotion/react';
3+
import CloseIcon from 'assets/icons/close.svg';
4+
import Dialog from 'rc-dialog';
5+
6+
const GlobalStyles = (theme: Theme) => `
7+
div.rc-dialog {
8+
font-family: ${theme.fonts.open.regular};
9+
font-size: ${theme.sizes.m};
10+
}
11+
div.rc-dialog-content {
12+
color: ${theme.colors.offWhite};
13+
background-color: ${theme.colors.blue};
14+
}
15+
div.rc-dialog-header {
16+
color: ${theme.colors.offWhite};
17+
background-color: ${theme.colors.blue};
18+
border-width: 0px;
19+
padding: 40px;
20+
}
21+
div.rc-dialog-title {
22+
font-size: ${theme.sizes.xxl};
23+
line-height: 42px;
24+
overflow: hidden;
25+
text-overflow: ellipsis;
26+
}
27+
button.rc-dialog-close {
28+
color: ${theme.colors.offWhite};
29+
font-size: ${theme.sizes.xxl};
30+
opacity: 1;
31+
top: 34px;
32+
right: 34px;
33+
width: 24px;
34+
height: 24px;
35+
padding: 0;
36+
background-color: ${theme.colors.offWhite};
37+
mask-image: url(${CloseIcon});
38+
padding: 0;
39+
40+
&:hover {
41+
opacity: 0.6;
42+
}
43+
}
44+
span.rc-dialog-close-x:after {
45+
content: "";
46+
}
47+
div.rc-dialog-body {
48+
padding: 0 40px 40px;
49+
}
50+
div.rc-dialog-mask {
51+
background-color: rgba(0, 0, 0, 0.8);
52+
}
53+
div.rc-dialog-footer {
54+
border-width: 0px;
55+
padding: 0 40px 40px;
56+
text-align: left;
57+
}
58+
`;
59+
60+
interface Props {
61+
title: string;
62+
visible: boolean;
63+
onClose: () => void;
64+
className?: string;
65+
}
66+
67+
const Modal: React.FC<Props> = ({ title, visible, onClose, className, children }) => {
68+
return (
69+
<Dialog
70+
title={title}
71+
animation="zoom"
72+
maskAnimation="fade"
73+
visible={visible}
74+
onClose={onClose}
75+
maskClosable
76+
className={className}
77+
>
78+
{children}
79+
<Global styles={GlobalStyles} />
80+
</Dialog>
81+
);
82+
};
83+
84+
export default Modal;

Diff for: app/src/components/connect/AddSession.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React, { useCallback, useState } from 'react';
22
import { observer } from 'mobx-react-lite';
3+
import * as LIT from 'types/generated/lit-sessions_pb';
34
import styled from '@emotion/styled';
45
import { usePrefixedTranslation } from 'hooks';
5-
import * as LIT from 'types/generated/lit-sessions_pb';
66
import { MAX_DATE } from 'util/constants';
77
import { useStore } from 'store';
88
import { Button, Column, HeaderFour, Row } from 'components/base';
@@ -55,7 +55,7 @@ const AddSession: React.FC<Props> = ({ primary }) => {
5555
? LIT.SessionType.TYPE_MACAROON_ADMIN
5656
: LIT.SessionType.TYPE_MACAROON_READONLY;
5757

58-
const session = await sessionStore.addSession(label, sessionType, MAX_DATE);
58+
const session = await sessionStore.addSession(label, sessionType, MAX_DATE, true);
5959

6060
if (session) {
6161
setLabel('');

Diff for: app/src/components/connect/ConnectPage.tsx

+36-16
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
import React from 'react';
1+
import React, { useCallback, useState } from 'react';
22
import { observer } from 'mobx-react-lite';
33
import styled from '@emotion/styled';
44
import nodeConnectSvg from 'assets/images/lightning-node-connect.svg';
55
import { usePrefixedTranslation } from 'hooks';
66
import { useStore } from 'store';
7-
import { Copy } from 'components/base';
7+
import { BoltOutlined, Copy, DisplayLarge, QRCode } from 'components/base';
88
import AddSession from './AddSession';
99
import PurpleButton from './PurpleButton';
10+
import QRCodeModal from './QRCodeModal';
1011
import SessionList from './SessionList';
1112

1213
const Styled = {
1314
Wrapper: styled.section`
1415
padding-top: 80px;
1516
`,
16-
DisplayLarge: styled.div`
17-
font-family: ${props => props.theme.fonts.open.semiBold};
18-
font-size: 32px;
19-
line-height: 40px;
20-
margin-top: 32px;
21-
margin-bottom: 16px;
22-
`,
2317
Description: styled.div`
2418
margin-bottom: 32px;
2519
`,
20+
Actions: styled.div`
21+
> a,
22+
> button {
23+
margin-right: 16px;
24+
}
25+
`,
2626
Divider: styled.div`
2727
max-width: 640px;
2828
border: 1px solid #384770;
@@ -32,29 +32,49 @@ const Styled = {
3232

3333
const ConnectPage: React.FC = () => {
3434
const { l } = usePrefixedTranslation('cmps.connect.ConnectPage');
35+
const [showQR, setShowQR] = useState(false);
3536
const { sessionStore } = useStore();
3637

37-
const { Wrapper, DisplayLarge, Description, Divider } = Styled;
38+
const toggleQRModal = useCallback(() => setShowQR(v => !v), []);
39+
40+
const { Wrapper, Description, Actions, Divider } = Styled;
3841
return !sessionStore.hasMultiple ? (
3942
<Wrapper>
4043
<img src={nodeConnectSvg} alt={l('pageTitle')} />
41-
<DisplayLarge>{l('pageTitle')}</DisplayLarge>
44+
<DisplayLarge space={16}>{l('pageTitle')}</DisplayLarge>
4245
<Description>
4346
{l('description1')}
4447
<br />
4548
{l('description2')}
4649
</Description>
47-
<PurpleButton onClick={sessionStore.copyFirstPhrase}>
48-
<Copy />
49-
{l('copyPhraseLabel')}
50-
</PurpleButton>
50+
<Actions>
51+
<a href={sessionStore.firstSessionTerminalUrl} target="_blank" rel="noreferrer">
52+
<PurpleButton>
53+
<BoltOutlined />
54+
{l('connectTerminalBtn')}
55+
</PurpleButton>
56+
</a>
57+
<PurpleButton secondary onClick={sessionStore.copyFirstPhrase}>
58+
<Copy />
59+
{l('copyPhraseBtn')}
60+
</PurpleButton>
61+
<PurpleButton secondary onClick={toggleQRModal}>
62+
<QRCode />
63+
{l('generateQrBtn')}
64+
</PurpleButton>
65+
<QRCodeModal
66+
url={sessionStore.firstSessionTerminalUrl}
67+
visible={showQR}
68+
onClose={toggleQRModal}
69+
/>
70+
</Actions>
5171
<Divider />
5272
<Description>{l('addlDesc')}</Description>
5373
<AddSession />
5474
</Wrapper>
5575
) : (
5676
<Wrapper>
57-
<DisplayLarge>{l('pageTitle')}</DisplayLarge>
77+
<DisplayLarge space={16}>{l('pageTitle')}</DisplayLarge>
5878
<Description>{l('description1')}</Description>
5979
<AddSession primary />
6080
<SessionList />

Diff for: app/src/components/connect/PurpleButton.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import styled from '@emotion/styled';
22

33
interface Props {
4+
secondary?: boolean;
45
tertiary?: boolean;
56
}
67

@@ -23,6 +24,18 @@ const PurpleButton = styled.button<Props>`
2324
outline: none;
2425
}
2526
27+
${props =>
28+
props.secondary &&
29+
`
30+
color: #252F4A;
31+
background-color: ${props.theme.colors.white};
32+
33+
&:hover {
34+
opacity: 0.8;
35+
background-color: ${props.theme.colors.white};
36+
}
37+
`}
38+
2639
${props =>
2740
props.tertiary &&
2841
`

Diff for: app/src/components/connect/QRCodeModal.tsx

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react';
2+
import styled from '@emotion/styled';
3+
import { usePrefixedTranslation } from 'hooks';
4+
import QRCodeImg from 'qrcode.react';
5+
import { Paragraph } from 'components/base';
6+
import Modal from 'components/common/Modal';
7+
8+
const Styled = {
9+
QRWrap: styled.div`
10+
display: inline-block;
11+
padding: 8px 8px 0;
12+
background-color: ${props => props.theme.colors.white};
13+
`,
14+
};
15+
16+
interface Props {
17+
url: string;
18+
visible: boolean;
19+
onClose: () => void;
20+
}
21+
22+
const QRCodeModal: React.FC<Props> = ({ url, visible, onClose }) => {
23+
const { l } = usePrefixedTranslation('cmps.connect.QRCodeModal');
24+
const { QRWrap } = Styled;
25+
return (
26+
<Modal title={l('title')} visible={visible} onClose={onClose}>
27+
<Paragraph space={32}>{l('desc')}</Paragraph>
28+
<QRWrap>
29+
<QRCodeImg value={url} size={500} />
30+
</QRWrap>
31+
</Modal>
32+
);
33+
};
34+
35+
export default QRCodeModal;

0 commit comments

Comments
 (0)