Skip to content

Commit 89c7f62

Browse files
committed
Merge branch 'frontend-swap-ui' into staging-swap
2 parents 2517eb5 + 7d9ae37 commit 89c7f62

File tree

6 files changed

+306
-12
lines changed

6 files changed

+306
-12
lines changed

frontends/web/src/components/groupedaccountselector/groupedaccountselector.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ export type TOption = {
4444
};
4545

4646
type TAccountSelector = {
47-
title: string;
47+
title?: string;
4848
disabled?: boolean;
4949
selected?: string;
5050
onChange: (value: string) => void;
51-
onProceed: () => void;
51+
onProceed?: () => void;
5252
accounts: TAccount[];
5353
};
5454

@@ -144,7 +144,9 @@ export const GroupedAccountSelector = ({ title, disabled, selected, onChange, on
144144

145145
return (
146146
<>
147-
<h1 className="title text-center">{title}</h1>
147+
{title && (
148+
<h1 className="title text-center">{title}</h1>
149+
)}
148150
<Select
149151
className={styles.select}
150152
classNamePrefix="react-select"
@@ -169,14 +171,16 @@ export const GroupedAccountSelector = ({ title, disabled, selected, onChange, on
169171
}}
170172
defaultValue={options[0]?.options[0]}
171173
/>
172-
<div className="buttons text-center">
173-
<Button
174-
primary
175-
onClick={onProceed}
176-
disabled={!selected || disabled}>
177-
{t('buy.info.next')}
178-
</Button>
179-
</div>
174+
{onProceed && (
175+
<div className="buttons text-center">
176+
<Button
177+
primary
178+
onClick={onProceed}
179+
disabled={!selected || disabled}>
180+
{t('buy.info.next')}
181+
</Button>
182+
</div>
183+
)}
180184
</>
181185

182186
);

frontends/web/src/locales/en/app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,8 @@
898898
"sell_crypto": "Sell crypto",
899899
"send": "Send",
900900
"spend_bitcoin": "Spend bitcoin",
901-
"spend_crypto": "Spend crypto"
901+
"spend_crypto": "Spend crypto",
902+
"swap": "Swap"
902903
},
903904
"genericError": "An error occurred. If you notice any issues, please restart the application.",
904905
"goal": {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright 2025 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the 'License');
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an 'AS IS' BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { ChangeEvent } from 'react';
18+
import type { AccountCode, TAccount } from '@/api/account';
19+
import { Grid, Column } from '@/components/layout';
20+
import { NumberInput } from '@/components/forms';
21+
import { GroupedAccountSelector } from '@/components/groupedaccountselector/groupedaccountselector';
22+
23+
type Props = {
24+
accountCode: AccountCode | undefined;
25+
accounts: TAccount[];
26+
id: string;
27+
onChangeAccountCode: (accountCode: AccountCode) => void;
28+
onChangeValue?: (value: string) => void;
29+
value: string | undefined;
30+
};
31+
32+
export const InputWithAccountSelector = ({
33+
accountCode,
34+
accounts,
35+
id,
36+
onChangeAccountCode,
37+
onChangeValue,
38+
value,
39+
}: Props) => {
40+
const hasAccounts = accounts && accounts.length > 0;
41+
return (
42+
<Grid col={hasAccounts ? '2' : '1'}>
43+
{hasAccounts && (
44+
<Column>
45+
<GroupedAccountSelector
46+
accounts={accounts}
47+
selected={accountCode}
48+
onChange={onChangeAccountCode}
49+
/>
50+
</Column>
51+
)}
52+
<Column>
53+
<NumberInput
54+
id={id}
55+
name={id}
56+
value={value}
57+
onChange={(event: ChangeEvent<HTMLInputElement>) => {
58+
onChangeValue && onChangeValue(event.target.value);
59+
}}
60+
/>
61+
</Column>
62+
</Grid>
63+
);
64+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Copyright 2025 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the 'License');
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an 'AS IS' BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Select, { components, SingleValueProps, OptionProps } from 'react-select';
18+
import { Label } from '@/components/forms';
19+
20+
type TOption<T = any> = {
21+
amount: string;
22+
icon: string;
23+
label: string;
24+
value: T;
25+
};
26+
27+
// shown when selected
28+
const CustomSingleValue = (props: SingleValueProps<TOption, false>) => {
29+
const { data } = props;
30+
return (
31+
<components.SingleValue {...props}>
32+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
33+
<img src={data.icon} style={{ width: 24, height: 24 }} />
34+
<span>{data.label}</span>
35+
<span>({data.amount})</span>
36+
</div>
37+
</components.SingleValue>
38+
);
39+
};
40+
41+
42+
// shown in dropdown
43+
const CustomOption = (props: OptionProps<TOption, false>) => {
44+
const { data, innerProps, isFocused, isSelected } = props;
45+
46+
return (
47+
<div
48+
{...innerProps}
49+
style={{
50+
backgroundColor: isFocused ? '#eee' : isSelected ? '#ddd' : 'white',
51+
}}
52+
>
53+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
54+
<img src={data.icon} style={{ width: 24, height: 24 }} />
55+
<span>{data.label}</span>
56+
</div>
57+
<span>{data.amount}</span>
58+
</div>
59+
);
60+
};
61+
62+
63+
export const SwapServiceSelector = () => {
64+
const options = [{
65+
amount: '1',
66+
icon: '',
67+
label: 'Thorchain',
68+
value: 'thor'
69+
}, {
70+
amount: '0.04',
71+
icon: '',
72+
label: 'Thorchain',
73+
value: 'thor'
74+
}];
75+
76+
return (
77+
<section>
78+
<Label>
79+
Swapping services
80+
</Label>
81+
<Select<TOption>
82+
components={{
83+
Option: CustomOption,
84+
SingleValue: CustomSingleValue,
85+
}}
86+
isSearchable={false}
87+
options={options}
88+
onChange={(option) => console.log(option?.value)}
89+
defaultValue={options[0]}
90+
/>
91+
</section>
92+
);
93+
};
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Copyright 2025 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useState } from 'react';
18+
import { useTranslation } from 'react-i18next';
19+
import type { AccountCode, TAccount } from '@/api/account';
20+
import { GuideWrapper, GuidedContent, Main, Header } from '@/components/layout';
21+
import { View, ViewButtons, ViewContent } from '@/components/view/view';
22+
import { SubTitle } from '@/components/title';
23+
import { Guide } from '@/components/guide/guide';
24+
import { Entry } from '@/components/guide/entry';
25+
import { Button, Label } from '@/components/forms';
26+
import { SwapServiceSelector } from './components/swap-service-selector';
27+
import { InputWithAccountSelector } from './components/input-with-account-selector';
28+
29+
type Props = {
30+
accounts: TAccount[];
31+
code: AccountCode;
32+
};
33+
34+
export const Swap = ({
35+
accounts,
36+
code,
37+
}: Props) => {
38+
const { t } = useTranslation();
39+
40+
// Send
41+
const [fromAccountCode, setFromAccountCode] = useState<string>();
42+
const [swapSendAmount, setSwapSendAmount] = useState<string>();
43+
// Receive
44+
const [toAccountCode, setToAccountCode] = useState<string>();
45+
const [swapReceiveAmount, setSwapReceiveAmount] = useState<string>();
46+
47+
console.log(code, setSwapReceiveAmount);
48+
49+
return (
50+
<GuideWrapper>
51+
<GuidedContent>
52+
<Main>
53+
<Header
54+
hideSidebarToggler
55+
title={
56+
<SubTitle>
57+
Swap
58+
</SubTitle>
59+
}
60+
/>
61+
<View
62+
fullscreen={false}
63+
width="600px"
64+
>
65+
<ViewContent>
66+
<Label
67+
htmlFor="swapSendAmount">
68+
<span>
69+
{t('generic.send')}
70+
</span>
71+
<Button transparent>
72+
<small>
73+
Max 0.12345678 BTC
74+
</small>
75+
</Button>
76+
</Label>
77+
78+
<InputWithAccountSelector
79+
accounts={accounts}
80+
id="swapSendAmount"
81+
accountCode={fromAccountCode}
82+
onChangeAccountCode={setFromAccountCode}
83+
value={swapSendAmount}
84+
onChangeValue={setSwapSendAmount}
85+
/>
86+
<Label
87+
htmlFor="swapGetAmount">
88+
<span>
89+
{t('generic.receiveWithoutCoinCode')}
90+
</span>
91+
<small>
92+
Max 45678 ETH
93+
</small>
94+
</Label>
95+
<InputWithAccountSelector
96+
accounts={accounts}
97+
id="swapGetAmount"
98+
accountCode={toAccountCode}
99+
onChangeAccountCode={setToAccountCode}
100+
value={swapReceiveAmount}
101+
/>
102+
<SwapServiceSelector />
103+
</ViewContent>
104+
<ViewButtons>
105+
<Button primary>
106+
{t('generic.swap')}
107+
</Button>
108+
<Button secondary>
109+
{t('button.back')}
110+
</Button>
111+
</ViewButtons>
112+
</View>
113+
</Main>
114+
</GuidedContent>
115+
<Guide>
116+
<Entry
117+
key="guide.settings.servers"
118+
entry={t('guide.settings.servers', { returnObjects: true })}
119+
/>
120+
</Guide>
121+
</GuideWrapper>
122+
);
123+
};

frontends/web/src/routes/router.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Pocket } from './market/pocket';
2626
import { BTCDirect } from './market/btcdirect';
2727
import { BTCDirectOTC } from './market/btcdirect-otc';
2828
import { Bitrefill } from './market/bitrefill';
29+
import { Swap } from './market/swap/swap';
2930
import { Info } from './account/info/info';
3031
import { Receive } from './account/receive/receive';
3132
import { SendWrapper } from './account/send/send-wrapper';
@@ -187,6 +188,12 @@ export const AppRouter = ({ devices, devicesKey, accounts, activeAccounts }: TAp
187188
region={''} />
188189
</InjectParams>);
189190

191+
const SwapEl = (<InjectParams>
192+
<Swap
193+
code={''}
194+
accounts={activeAccounts} />
195+
</InjectParams>);
196+
190197
const MarketEl = (<InjectParams>
191198
<Market
192199
accounts={activeAccounts}
@@ -287,6 +294,8 @@ export const AppRouter = ({ devices, devicesKey, accounts, activeAccounts }: TAp
287294
<Route path="pocket/sell/:code/:region" element={PocketSellEl} />
288295
<Route path="select/:code" element={MarketEl} />
289296
<Route path="btcdirect-otc" element={<BTCDirectOTC/>} />
297+
<Route path="swap" element={SwapEl} />
298+
<Route path="swap/:code" element={SwapEl} />
290299
</Route>
291300
<Route path="manage-backups/:deviceID" element={ManageBackupsEl} />
292301
<Route path="accounts/select-receive" element={ReceiveAccountsSelectorEl} />

0 commit comments

Comments
 (0)