feat: captcha
This commit is contained in:
parent
b79ee0c08a
commit
9a99f934b6
12 changed files with 184 additions and 11 deletions
|
|
@ -8,6 +8,9 @@
|
|||
import { Send, DollarSign, Coins, Loader2 } from 'lucide-svelte';
|
||||
import { PORTFOLIO_DATA } from '$lib/stores/portfolio-data';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Turnstile } from 'svelte-turnstile';
|
||||
import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
|
|
@ -24,6 +27,9 @@
|
|||
let amount = $state('');
|
||||
let selectedCoinSymbol = $state('');
|
||||
let loading = $state(false);
|
||||
let turnstileToken = $state('');
|
||||
let turnstileError = $state('');
|
||||
let turnstileReset = $state<(() => void) | undefined>(undefined);
|
||||
|
||||
let numericAmount = $derived(parseFloat(amount) || 0);
|
||||
let hasValidAmount = $derived(numericAmount > 0);
|
||||
|
|
@ -57,6 +63,9 @@
|
|||
|
||||
let isWithinCoinValueLimit = $derived(transferType === 'COIN' ? estimatedValue >= 10 : true);
|
||||
|
||||
const turnstileVerified = $derived(!!$page.data?.turnstileVerified);
|
||||
let optimisticTurnstileVerified = $state(false);
|
||||
|
||||
let canSend = $derived(
|
||||
hasValidAmount &&
|
||||
hasValidRecipient &&
|
||||
|
|
@ -64,7 +73,8 @@
|
|||
isWithinCashLimit &&
|
||||
isWithinCoinValueLimit &&
|
||||
!loading &&
|
||||
(transferType === 'CASH' || selectedCoinSymbol.length > 0)
|
||||
(transferType === 'CASH' || selectedCoinSymbol.length > 0) &&
|
||||
(turnstileVerified || optimisticTurnstileVerified || !!turnstileToken)
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
|
|
@ -114,7 +124,8 @@
|
|||
recipientUsername: recipientUsername.trim(),
|
||||
type: transferType,
|
||||
amount: numericAmount,
|
||||
coinSymbol: transferType === 'COIN' ? selectedCoinSymbol : undefined
|
||||
coinSymbol: transferType === 'COIN' ? selectedCoinSymbol : undefined,
|
||||
turnstileToken
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -141,6 +152,9 @@
|
|||
|
||||
onSuccess?.();
|
||||
handleClose();
|
||||
|
||||
turnstileToken = '';
|
||||
optimisticTurnstileVerified = true;
|
||||
} catch (e) {
|
||||
toast.error('Transfer failed', {
|
||||
description: (e as Error).message
|
||||
|
|
@ -325,6 +339,34 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !(turnstileVerified || optimisticTurnstileVerified)}
|
||||
<div>
|
||||
<Turnstile
|
||||
siteKey={PUBLIC_TURNSTILE_SITE_KEY}
|
||||
theme="auto"
|
||||
size="normal"
|
||||
bind:reset={turnstileReset}
|
||||
on:callback={(e: CustomEvent<{ token: string }>) => {
|
||||
turnstileToken = e.detail.token;
|
||||
turnstileError = '';
|
||||
}}
|
||||
on:error={(e: CustomEvent<{ code: string }>) => {
|
||||
turnstileToken = '';
|
||||
turnstileError = e.detail.code || 'Captcha error';
|
||||
}}
|
||||
on:expired={() => {
|
||||
turnstileToken = '';
|
||||
turnstileError = 'Captcha expired';
|
||||
}}
|
||||
execution="render"
|
||||
appearance="always"
|
||||
/>
|
||||
{#if turnstileError}
|
||||
<p class="text-destructive mt-1 text-xs">{turnstileError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
import { TrendingUp, TrendingDown, Loader2 } from 'lucide-svelte';
|
||||
import { PORTFOLIO_SUMMARY } from '$lib/stores/portfolio-data';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Turnstile } from 'svelte-turnstile';
|
||||
import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
|
|
@ -24,6 +27,8 @@
|
|||
|
||||
let amount = $state('');
|
||||
let loading = $state(false);
|
||||
let turnstileToken = $state('');
|
||||
let turnstileError = $state('');
|
||||
|
||||
let numericAmount = $derived(parseFloat(amount) || 0);
|
||||
let currentPrice = $derived(coin.currentPrice || 0);
|
||||
|
|
@ -39,7 +44,14 @@
|
|||
let hasEnoughFunds = $derived(
|
||||
type === 'BUY' ? numericAmount <= userBalance : numericAmount <= userHolding
|
||||
);
|
||||
let canTrade = $derived(hasValidAmount && hasEnoughFunds && !loading);
|
||||
const turnstileVerified = $derived(!!$page.data?.turnstileVerified);
|
||||
let optimisticTurnstileVerified = $state(false);
|
||||
|
||||
let showCaptcha = $derived(!(turnstileVerified || optimisticTurnstileVerified));
|
||||
|
||||
let canTrade = $derived(
|
||||
hasValidAmount && hasEnoughFunds && !loading && (!showCaptcha || !!turnstileToken)
|
||||
);
|
||||
|
||||
function calculateEstimate(amount: number, tradeType: 'BUY' | 'SELL', price: number) {
|
||||
if (!amount || !price || !coin) return { result: 0 };
|
||||
|
|
@ -70,6 +82,8 @@
|
|||
loading = false;
|
||||
}
|
||||
|
||||
let turnstileReset = $state<(() => void) | undefined>(undefined);
|
||||
|
||||
async function handleTrade() {
|
||||
if (!canTrade) return;
|
||||
|
||||
|
|
@ -82,7 +96,8 @@
|
|||
},
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
amount: numericAmount
|
||||
amount: numericAmount,
|
||||
turnstileToken
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -101,6 +116,9 @@
|
|||
|
||||
onSuccess?.();
|
||||
handleClose();
|
||||
|
||||
turnstileToken = '';
|
||||
optimisticTurnstileVerified = true;
|
||||
} catch (e) {
|
||||
toast.error('Trade failed', {
|
||||
description: (e as Error).message
|
||||
|
|
@ -194,6 +212,33 @@
|
|||
{type === 'BUY' ? 'Insufficient funds' : 'Insufficient coins'}
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<Turnstile
|
||||
siteKey={PUBLIC_TURNSTILE_SITE_KEY}
|
||||
theme="auto"
|
||||
size="normal"
|
||||
bind:reset={turnstileReset}
|
||||
on:callback={(e: CustomEvent<{ token: string }>) => {
|
||||
turnstileToken = e.detail.token;
|
||||
turnstileError = '';
|
||||
}}
|
||||
on:error={(e: CustomEvent<{ code: string }>) => {
|
||||
turnstileToken = '';
|
||||
turnstileError = e.detail.code || 'Captcha error';
|
||||
}}
|
||||
on:expired={() => {
|
||||
turnstileToken = '';
|
||||
turnstileError = 'Captcha expired';
|
||||
}}
|
||||
execution="render"
|
||||
appearance="always"
|
||||
/>
|
||||
{#if turnstileError}
|
||||
<p class="text-destructive mt-1 text-xs">{turnstileError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="flex gap-2">
|
||||
|
|
|
|||
Reference in a new issue