Skip to content

Commit cef8466

Browse files
feat(frontend): handle WC connect in ScannerCode
1 parent 1b468de commit cef8466

File tree

2 files changed

+110
-57
lines changed

2 files changed

+110
-57
lines changed

src/frontend/src/lib/components/scanner/ScannerCode.svelte

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77
import { feeRatePercentilesStore } from '$btc/stores/fee-rate-percentiles.store';
88
import { OCP_PAY_WITH_BTC_ENABLED } from '$env/open-crypto-pay.env';
99
import IconChain from '$lib/components/icons/IconChain.svelte';
10+
import IconHelpCircle from '$lib/components/icons/IconHelpCircle.svelte';
1011
import QrCodeScanner from '$lib/components/qr/QrCodeScanner.svelte';
1112
import ScannerCodeInput from '$lib/components/scanner/ScannerCodeInput.svelte';
1213
import BottomSheet from '$lib/components/ui/BottomSheet.svelte';
1314
import Button from '$lib/components/ui/Button.svelte';
14-
import ButtonGroup from '$lib/components/ui/ButtonGroup.svelte';
15-
import ContentWithToolbar from '$lib/components/ui/ContentWithToolbar.svelte';
1615
import Responsive from '$lib/components/ui/Responsive.svelte';
1716
import { OPEN_CRYPTO_PAY_ENTER_MANUALLY_BUTTON } from '$lib/constants/test-ids.constants';
1817
import { btcAddressMainnet } from '$lib/derived/address.derived';
@@ -27,14 +26,18 @@
2726
import { PAY_CONTEXT_KEY, type PayContext } from '$lib/stores/open-crypto-pay.store';
2827
import type { QrStatus } from '$lib/types/qr-code';
2928
import { ScannerResults } from '$lib/types/scanner';
29+
import { replaceOisyPlaceholders } from '$lib/utils/i18n.utils';
3030
import { prepareBasePayableTokens } from '$lib/utils/open-crypto-pay.utils';
3131
import { waitReady } from '$lib/utils/timeout.utils';
3232
3333
interface Props {
34-
onNext: (results: ScannerResults) => void;
34+
onNext: (params: { results: ScannerResults; code?: string }) => void;
35+
onOpenInfo?: () => void;
3536
}
3637
37-
let { onNext }: Props = $props();
38+
let { onNext, onOpenInfo }: Props = $props();
39+
40+
const WALLET_CONNECT_URI_PREFIX = 'wc:';
3841
3942
let openBottomSheet = $state(false);
4043
let uri = $state('');
@@ -44,6 +47,11 @@
4447
const { setData, setAvailableTokens } = getContext<PayContext>(PAY_CONTEXT_KEY);
4548
4649
const processCode = async (code: string) => {
50+
if (code.startsWith(WALLET_CONNECT_URI_PREFIX)) {
51+
onNext({ results: ScannerResults.WALLET_CONNECT, code });
52+
return;
53+
}
54+
4755
busy.start();
4856
4957
error = '';
@@ -74,7 +82,7 @@
7482
7583
setAvailableTokens(tokensWithFees);
7684
77-
onNext(ScannerResults.PAY);
85+
onNext({ results: ScannerResults.PAY });
7886
} catch (_: unknown) {
7987
error = $i18n.scanner.error.code_link_is_not_valid;
8088
} finally {
@@ -101,67 +109,67 @@
101109
});
102110
</script>
103111

104-
<ContentWithToolbar styleClass="flex flex-col gap-3 md:gap-4 w-full">
105-
<QrCodeScanner onScan={handleScan} />
112+
<div class="relative flex w-full flex-col bg-tertiary">
113+
<QrCodeScanner expandedLayout onScan={handleScan} />
106114

107115
<Responsive up="md">
108116
<ScannerCodeInput
109117
name="uri"
110118
{error}
111119
label={$i18n.scanner.text.url_or_code}
112120
placeholder={$i18n.scanner.text.enter_or_paste_code}
121+
styleClass="absolute right-0 bottom-[90px] left-0 mx-auto w-[90%] rounded-lg bg-surface"
113122
bind:value={uri}
114-
/>
123+
>
124+
<Button disabled={isEmptyUri} fullWidth onclick={handleManualConnect} paddingSmall>
125+
{$i18n.core.text.continue}
126+
</Button>
127+
</ScannerCodeInput>
115128
</Responsive>
116129

117130
<Responsive down="sm">
118131
<BottomSheet contentClass="min-h-[10vh]" bind:visible={openBottomSheet}>
119132
{#snippet content()}
120-
<div class="mb-4">
121-
<ScannerCodeInput
122-
name="uri"
123-
{error}
124-
label={$i18n.scanner.text.url_or_code}
125-
placeholder={$i18n.scanner.text.enter_or_paste_code}
126-
bind:value={uri}
127-
/>
128-
</div>
129-
{/snippet}
130-
131-
{#snippet footer()}
132-
<Button disabled={isEmptyUri} fullWidth onclick={handleManualConnect}>
133-
{$i18n.core.text.continue}
134-
</Button>
133+
<ScannerCodeInput
134+
name="uri"
135+
{error}
136+
label={$i18n.scanner.text.url_or_code}
137+
placeholder={$i18n.scanner.text.enter_or_paste_code}
138+
bind:value={uri}
139+
>
140+
<Button disabled={isEmptyUri} fullWidth onclick={handleManualConnect} paddingSmall>
141+
{$i18n.core.text.continue}
142+
</Button>
143+
</ScannerCodeInput>
135144
{/snippet}
136145
</BottomSheet>
146+
147+
<div class="absolute right-0 bottom-[90px] left-0 mx-auto flex w-[200px] justify-center">
148+
<Button
149+
colorStyle="tertiary"
150+
innerStyleClass="flex items-center justify-center"
151+
onclick={() => {
152+
uri = '';
153+
error = '';
154+
openBottomSheet = true;
155+
}}
156+
testId={OPEN_CRYPTO_PAY_ENTER_MANUALLY_BUTTON}
157+
>
158+
{$i18n.scanner.text.enter_manually}
159+
160+
<IconChain />
161+
</Button>
162+
</div>
137163
</Responsive>
138164

139-
{#snippet toolbar()}
140-
<Responsive up="md">
141-
<ButtonGroup>
142-
<Button disabled={isEmptyUri} onclick={handleManualConnect}>
143-
{$i18n.core.text.continue}
144-
</Button>
145-
</ButtonGroup>
146-
</Responsive>
147-
148-
<Responsive down="sm">
149-
<div class="mb-4 flex flex-0">
150-
<Button
151-
colorStyle="primary"
152-
innerStyleClass="flex items-center justify-center"
153-
link
154-
onclick={() => {
155-
uri = '';
156-
error = '';
157-
openBottomSheet = true;
158-
}}
159-
testId={OPEN_CRYPTO_PAY_ENTER_MANUALLY_BUTTON}
160-
>
161-
{$i18n.scanner.text.enter_manually}
162-
<IconChain />
163-
</Button>
164-
</div>
165-
</Responsive>
166-
{/snippet}
167-
</ContentWithToolbar>
165+
<Button
166+
fullWidth
167+
link
168+
onclick={onOpenInfo}
169+
styleClass="text-secondary bg-surface py-4 rounded-none"
170+
>
171+
<span>{replaceOisyPlaceholders($i18n.scanner.text.what_is_scan)}</span>
172+
173+
<IconHelpCircle />
174+
</Button>
175+
</div>

src/frontend/src/tests/lib/components/scanner/ScannerCode.spec.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,13 @@ describe('ScannerCode.svelte', () => {
184184
} as PayableTokenWithFees
185185
];
186186

187+
const mockOnOpenInfo = vi.fn();
188+
187189
const renderWithContext = () =>
188190
render(ScannerCode, {
189191
props: {
190-
onNext: mockOnNext
192+
onNext: mockOnNext,
193+
onOpenInfo: mockOnOpenInfo
191194
},
192195
context: new Map([
193196
[
@@ -293,7 +296,49 @@ describe('ScannerCode.svelte', () => {
293296

294297
await waitFor(() => {
295298
expect(mockSetData).toHaveBeenCalledExactlyOnceWith(mockApiResponse);
296-
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith(ScannerResults.PAY);
299+
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith({ results: ScannerResults.PAY });
300+
});
301+
});
302+
303+
it('should call onNext with WALLET_CONNECT result for wc: URIs', async () => {
304+
renderWithContext();
305+
306+
await openManualEntry();
307+
308+
const input = await screen.findByPlaceholderText(en.scanner.text.enter_or_paste_code);
309+
await fireEvent.input(input, { target: { value: 'wc:abc123@2?relay-protocol=irn' } });
310+
311+
const button = screen.getByRole('button', { name: en.core.text.continue });
312+
await fireEvent.click(button);
313+
314+
await waitFor(() => {
315+
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith({
316+
results: ScannerResults.WALLET_CONNECT,
317+
code: 'wc:abc123@2?relay-protocol=irn'
318+
});
319+
});
320+
321+
expect(openCryptoPayServices.processOpenCryptoPayCode).not.toHaveBeenCalled();
322+
});
323+
324+
it('should not treat non-wc: URIs as WalletConnect', async () => {
325+
vi.mocked(openCryptoPayServices.processOpenCryptoPayCode).mockResolvedValue(mockApiResponse);
326+
327+
renderWithContext();
328+
329+
await openManualEntry();
330+
331+
const input = await screen.findByPlaceholderText(en.scanner.text.enter_or_paste_code);
332+
await fireEvent.input(input, { target: { value: 'https://example.com/pay' } });
333+
334+
const button = screen.getByRole('button', { name: en.core.text.continue });
335+
await fireEvent.click(button);
336+
337+
await waitFor(() => {
338+
expect(openCryptoPayServices.processOpenCryptoPayCode).toHaveBeenCalledExactlyOnceWith(
339+
'https://example.com/pay'
340+
);
341+
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith({ results: ScannerResults.PAY });
297342
});
298343
});
299344

@@ -443,7 +488,7 @@ describe('ScannerCode.svelte', () => {
443488
await fireEvent.click(button);
444489
await waitFor(() => {
445490
expect(mockSetsetAvailableTokens).toHaveBeenCalledExactlyOnceWith([]);
446-
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith(ScannerResults.PAY);
491+
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith({ results: ScannerResults.PAY });
447492
});
448493
});
449494
});
@@ -486,7 +531,7 @@ describe('ScannerCode.svelte', () => {
486531

487532
await waitFor(() => {
488533
expect(mockSetData).toHaveBeenCalledExactlyOnceWith(mockApiResponse);
489-
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith(ScannerResults.PAY);
534+
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith({ results: ScannerResults.PAY });
490535
});
491536
});
492537

@@ -505,7 +550,7 @@ describe('ScannerCode.svelte', () => {
505550

506551
await waitFor(() => {
507552
expect(mockSetData).toHaveBeenCalledExactlyOnceWith(mockApiResponse);
508-
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith(ScannerResults.PAY);
553+
expect(mockOnNext).toHaveBeenCalledExactlyOnceWith({ results: ScannerResults.PAY });
509554
});
510555
});
511556
});

0 commit comments

Comments
 (0)