mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-21 23:37:55 +00:00
feat: automate deposit flow + fix listUsers pagination
- Rewrite DepositWithdrawModal to send TX automatically via assetHubApi instead of manual copy-paste-hash flow - Fix listUsers pagination bug (default 50) in 4 edge functions by adding perPage: 1000 - fixes P2P offers not showing for users - Add new i18n keys for automated deposit states in all 6 languages
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
X,
|
||||
Copy,
|
||||
Check,
|
||||
ArrowDownToLine,
|
||||
ArrowUpFromLine,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Wallet,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
@@ -36,7 +35,7 @@ interface DepositWithdrawModalProps {
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
type DepositStep = 'form' | 'verifying' | 'success' | 'error';
|
||||
type DepositStep = 'form' | 'sending' | 'verifying' | 'success' | 'error';
|
||||
|
||||
export function DepositWithdrawModal({
|
||||
isOpen,
|
||||
@@ -46,7 +45,7 @@ export function DepositWithdrawModal({
|
||||
onSuccess,
|
||||
}: DepositWithdrawModalProps) {
|
||||
const { sessionToken } = useAuth();
|
||||
const { address } = useWallet();
|
||||
const { address, isConnected, keypair, assetHubApi } = useWallet();
|
||||
const { t, isRTL } = useTranslation();
|
||||
const { hapticImpact, hapticNotification } = useTelegram();
|
||||
|
||||
@@ -55,14 +54,11 @@ export function DepositWithdrawModal({
|
||||
// Deposit state
|
||||
const [depositToken, setDepositToken] = useState<'HEZ' | 'PEZ'>('HEZ');
|
||||
const [depositAmount, setDepositAmount] = useState('');
|
||||
const [txHash, setTxHash] = useState('');
|
||||
const [blockNumber, setBlockNumber] = useState('');
|
||||
const [depositStep, setDepositStep] = useState<DepositStep>('form');
|
||||
const [depositError, setDepositError] = useState('');
|
||||
const [depositResult, setDepositResult] = useState<{ amount: number; token: string } | null>(
|
||||
null
|
||||
);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Withdraw state
|
||||
const [withdrawToken, setWithdrawToken] = useState<'HEZ' | 'PEZ'>('HEZ');
|
||||
@@ -72,28 +68,10 @@ export function DepositWithdrawModal({
|
||||
const [withdrawError, setWithdrawError] = useState('');
|
||||
const [withdrawSuccess, setWithdrawSuccess] = useState(false);
|
||||
|
||||
const handleCopyAddress = useCallback(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(PLATFORM_WALLET);
|
||||
setCopied(true);
|
||||
hapticNotification('success');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Fallback for environments where clipboard API is not available
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = PLATFORM_WALLET;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
setCopied(true);
|
||||
hapticNotification('success');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
}, [hapticNotification]);
|
||||
const canDeposit = isConnected && !!keypair && !!assetHubApi;
|
||||
|
||||
const handleVerifyDeposit = async () => {
|
||||
if (!sessionToken || !txHash || !depositAmount) return;
|
||||
const handleDeposit = async () => {
|
||||
if (!sessionToken || !depositAmount || !assetHubApi || !keypair) return;
|
||||
|
||||
const amount = parseFloat(depositAmount);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
@@ -102,17 +80,54 @@ export function DepositWithdrawModal({
|
||||
}
|
||||
|
||||
hapticImpact('medium');
|
||||
setDepositStep('verifying');
|
||||
setDepositStep('sending');
|
||||
setDepositError('');
|
||||
|
||||
try {
|
||||
const result = await verifyDeposit(
|
||||
sessionToken,
|
||||
txHash.trim(),
|
||||
depositToken,
|
||||
amount,
|
||||
blockNumber ? parseInt(blockNumber) : undefined
|
||||
);
|
||||
const amountPlanck = BigInt(Math.floor(amount * 1e12));
|
||||
|
||||
// Create transaction on Asset Hub
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let tx: any;
|
||||
if (depositToken === 'HEZ') {
|
||||
// Native token transfer on Asset Hub
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tx = (assetHubApi.tx.balances as any).transferKeepAlive(
|
||||
PLATFORM_WALLET,
|
||||
amountPlanck.toString()
|
||||
);
|
||||
} else {
|
||||
// PEZ = asset ID 1 on Asset Hub
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tx = (assetHubApi.tx.assets as any).transfer(1, PLATFORM_WALLET, amountPlanck.toString());
|
||||
}
|
||||
|
||||
// Send TX and wait for finalization
|
||||
const txHash = await new Promise<string>((resolve, reject) => {
|
||||
tx.signAndSend(
|
||||
keypair,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(result: any) => {
|
||||
if (result.dispatchError) {
|
||||
if (result.dispatchError.isModule) {
|
||||
const decoded = assetHubApi!.registry.findMetaError(result.dispatchError.asModule);
|
||||
reject(new Error(`${decoded.section}.${decoded.name}`));
|
||||
} else {
|
||||
reject(new Error(result.dispatchError.toString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (result.status.isFinalized) {
|
||||
resolve(result.txHash.toHex());
|
||||
}
|
||||
}
|
||||
).catch(reject);
|
||||
});
|
||||
|
||||
// TX finalized, now verify deposit on backend
|
||||
setDepositStep('verifying');
|
||||
|
||||
const result = await verifyDeposit(sessionToken, txHash, depositToken, amount);
|
||||
|
||||
setDepositResult({ amount: result.amount, token: result.token });
|
||||
setDepositStep('success');
|
||||
@@ -168,8 +183,6 @@ export function DepositWithdrawModal({
|
||||
setDepositStep('form');
|
||||
setDepositError('');
|
||||
setDepositResult(null);
|
||||
setTxHash('');
|
||||
setBlockNumber('');
|
||||
setDepositAmount('');
|
||||
};
|
||||
|
||||
@@ -247,35 +260,23 @@ export function DepositWithdrawModal({
|
||||
{/* ── Deposit Tab ── */}
|
||||
{activeTab === 'deposit' && (
|
||||
<>
|
||||
{depositStep === 'form' && (
|
||||
{!canDeposit ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-3">
|
||||
<Wallet className="w-12 h-12 text-muted-foreground/50" />
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{t('p2p.walletNotConnected')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{t('p2p.connectWalletFirst')}
|
||||
</p>
|
||||
</div>
|
||||
) : depositStep === 'form' ? (
|
||||
<>
|
||||
{/* Instructions */}
|
||||
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-xl p-3">
|
||||
<p className="text-xs text-cyan-300">{t('p2p.depositInstructions')}</p>
|
||||
</div>
|
||||
|
||||
{/* Platform wallet address */}
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
{t('p2p.platformWallet')}
|
||||
</label>
|
||||
<div className="flex items-center gap-2 bg-muted rounded-lg p-2.5">
|
||||
<code className="flex-1 text-xs text-foreground break-all font-mono">
|
||||
{PLATFORM_WALLET}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyAddress}
|
||||
className="shrink-0 p-1.5 rounded-md hover:bg-card/50 transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token select */}
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
@@ -314,47 +315,24 @@ export function DepositWithdrawModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* TX Hash */}
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
{t('p2p.txHash')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={txHash}
|
||||
onChange={(e) => setTxHash(e.target.value)}
|
||||
placeholder={t('p2p.txHashPlaceholder')}
|
||||
className="w-full bg-muted rounded-lg px-3 py-2.5 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-cyan-500 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Block Number (optional) */}
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
{t('p2p.blockNumber')} ({t('p2p.optional')})
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
value={blockNumber}
|
||||
onChange={(e) => setBlockNumber(e.target.value)}
|
||||
placeholder="e.g. 1234567"
|
||||
className="w-full bg-muted rounded-lg px-3 py-2.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-cyan-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Verify button */}
|
||||
{/* Deposit button */}
|
||||
<button
|
||||
onClick={handleVerifyDeposit}
|
||||
disabled={!txHash || !depositAmount || !sessionToken}
|
||||
onClick={handleDeposit}
|
||||
disabled={!depositAmount || !sessionToken}
|
||||
className="w-full py-3 bg-cyan-500 hover:bg-cyan-600 disabled:bg-muted disabled:text-muted-foreground text-white font-medium rounded-xl transition-colors"
|
||||
>
|
||||
{t('p2p.verifyDeposit')}
|
||||
{t('p2p.deposit')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{depositStep === 'verifying' && (
|
||||
) : depositStep === 'sending' ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-3">
|
||||
<Loader2 className="w-10 h-10 text-cyan-400 animate-spin" />
|
||||
<p className="text-sm text-foreground">{t('p2p.depositSending')}</p>
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{t('p2p.depositSendingDesc')}
|
||||
</p>
|
||||
</div>
|
||||
) : depositStep === 'verifying' ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-3">
|
||||
<Loader2 className="w-10 h-10 text-cyan-400 animate-spin" />
|
||||
<p className="text-sm text-foreground">{t('p2p.verifying')}</p>
|
||||
@@ -362,9 +340,7 @@ export function DepositWithdrawModal({
|
||||
{t('p2p.verifyingDesc')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{depositStep === 'success' && depositResult && (
|
||||
) : depositStep === 'success' && depositResult ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-3">
|
||||
<CheckCircle2 className="w-12 h-12 text-green-400" />
|
||||
<p className="text-lg font-bold text-foreground">{t('p2p.depositSuccess')}</p>
|
||||
@@ -378,9 +354,7 @@ export function DepositWithdrawModal({
|
||||
{t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{depositStep === 'error' && (
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-3">
|
||||
<AlertCircle className="w-12 h-12 text-red-400" />
|
||||
<p className="text-lg font-bold text-foreground">{t('p2p.depositFailed')}</p>
|
||||
|
||||
@@ -408,8 +408,13 @@ const ar: Translations = {
|
||||
verifyingDesc: 'يتم فحص المعاملة على السلسلة. قد يستغرق هذا حتى ٦٠ ثانية.',
|
||||
depositSuccess: 'تم الإيداع بنجاح!',
|
||||
depositFailed: 'فشل الإيداع',
|
||||
depositInstructions: 'أرسل التوكنات إلى عنوان محفظة المنصة أدناه، ثم الصق هاش المعاملة للتحقق.',
|
||||
depositInstructions:
|
||||
'اختر توكن وأدخل المبلغ للإيداع في رصيد P2P الخاص بك. سيتم إرسال المعاملة من محفظتك تلقائياً.',
|
||||
depositInvalidAmount: 'يرجى إدخال مبلغ صالح',
|
||||
depositSending: 'جاري إرسال المعاملة...',
|
||||
depositSendingDesc: 'يتم توقيع وإرسال إيداعك إلى Asset Hub. قد يستغرق هذا حتى 30 ثانية.',
|
||||
walletNotConnected: 'المحفظة غير متصلة',
|
||||
connectWalletFirst: 'يرجى فتح قفل محفظتك للإيداع.',
|
||||
selectToken: 'اختر التوكن',
|
||||
withdrawAmount: 'مبلغ السحب',
|
||||
networkFee: 'رسوم الشبكة',
|
||||
|
||||
@@ -411,8 +411,13 @@ const ckb: Translations = {
|
||||
depositSuccess: 'پارەدانان سەرکەوتوو بوو!',
|
||||
depositFailed: 'پارەدانان سەرنەکەوت',
|
||||
depositInstructions:
|
||||
'تۆکن بنێرە بۆ ناونیشانی جزدانی پلاتفۆرمی خوارەوە، پاشان هاشی مامەڵە بلکێنە بۆ پشتڕاستکردنەوە.',
|
||||
'تۆکنێک هەڵبژێرە و بڕەکە بنووسە بۆ زیادکردن لە باڵانسی P2P. مامەڵەکە بە شێوەیەکی خۆکار لە جزدانەکەت دەنێردرێت.',
|
||||
depositInvalidAmount: 'تکایە بڕێکی دروست بنووسە',
|
||||
depositSending: 'مامەڵە دەنێردرێت...',
|
||||
depositSendingDesc:
|
||||
'پارەدانانەکەت لە Asset Hub واژووکراوە و دەنێردرێت. ئەمە لەوانەیە تا ٣٠ چرکە بخایەنێت.',
|
||||
walletNotConnected: 'جزدان پەیوەندی نەکراوە',
|
||||
connectWalletFirst: 'تکایە بۆ پارەدانان قفڵی جزدانەکەت بکەرەوە.',
|
||||
selectToken: 'تۆکن هەڵبژێرە',
|
||||
withdrawAmount: 'بڕی دەرهێنان',
|
||||
networkFee: 'کرێی تۆڕ',
|
||||
|
||||
@@ -410,8 +410,13 @@ const en: Translations = {
|
||||
depositSuccess: 'Deposit Successful!',
|
||||
depositFailed: 'Deposit Failed',
|
||||
depositInstructions:
|
||||
'Send tokens to the platform wallet address below, then paste the transaction hash to verify.',
|
||||
'Select a token and enter the amount to deposit to your P2P balance. The transaction will be sent from your wallet automatically.',
|
||||
depositInvalidAmount: 'Please enter a valid amount',
|
||||
depositSending: 'Sending transaction...',
|
||||
depositSendingDesc:
|
||||
'Signing and sending your deposit to Asset Hub. This may take up to 30 seconds.',
|
||||
walletNotConnected: 'Wallet not connected',
|
||||
connectWalletFirst: 'Please unlock your wallet to make deposits.',
|
||||
selectToken: 'Select Token',
|
||||
withdrawAmount: 'Withdrawal Amount',
|
||||
networkFee: 'Network Fee',
|
||||
|
||||
@@ -410,8 +410,13 @@ const fa: Translations = {
|
||||
depositSuccess: 'واریز موفق!',
|
||||
depositFailed: 'واریز ناموفق',
|
||||
depositInstructions:
|
||||
'توکنها را به آدرس کیف پول پلتفرم زیر ارسال کنید، سپس هش تراکنش را برای تایید جایگذاری کنید.',
|
||||
'یک توکن انتخاب کنید و مقدار را وارد کنید تا به موجودی P2P شما اضافه شود. تراکنش به صورت خودکار از کیف پول شما ارسال خواهد شد.',
|
||||
depositInvalidAmount: 'لطفا مقدار معتبری وارد کنید',
|
||||
depositSending: 'در حال ارسال تراکنش...',
|
||||
depositSendingDesc:
|
||||
'واریز شما در حال امضا و ارسال به Asset Hub است. این عملیات ممکن است تا ۳۰ ثانیه طول بکشد.',
|
||||
walletNotConnected: 'کیف پول متصل نیست',
|
||||
connectWalletFirst: 'لطفا برای واریز، قفل کیف پول خود را باز کنید.',
|
||||
selectToken: 'انتخاب توکن',
|
||||
withdrawAmount: 'مقدار برداشت',
|
||||
networkFee: 'کارمزد شبکه',
|
||||
|
||||
@@ -423,8 +423,13 @@ const krd: Translations = {
|
||||
depositSuccess: 'Depo Serkeftî!',
|
||||
depositFailed: 'Depo Serneket',
|
||||
depositInstructions:
|
||||
'Token bişîne navnîşana cûzdanê ya platformê ya jêrîn, paşê hash ya danûstandinê bişkoje ji bo verastkirin.',
|
||||
'Ji bo ku hûn li balanseya P2P-ê ya xwe depo bikin, tokenek hilbijêrin û mîqdarê binivîsin. Danûstandin dê ji cûzdanê we bixweber were şandin.',
|
||||
depositInvalidAmount: 'Ji kerema xwe mîqdarek derbasdar binivîsin',
|
||||
depositSending: 'Danûstandin tê şandin...',
|
||||
depositSendingDesc:
|
||||
'Depoya we li Asset Hub tê îmzekirin û şandin. Ev dibe ku heta 30 saniyeyan bigire.',
|
||||
walletNotConnected: 'Cûzdan ve negirêdayî ye',
|
||||
connectWalletFirst: 'Ji bo depokirinê, ji kerema xwe kilîda cûzdanê vekin.',
|
||||
selectToken: 'Token Hilbijêrin',
|
||||
withdrawAmount: 'Mîqdara Derxistinê',
|
||||
networkFee: 'Heqê Torê',
|
||||
|
||||
@@ -410,8 +410,13 @@ const tr: Translations = {
|
||||
depositSuccess: 'Yatırma Başarılı!',
|
||||
depositFailed: 'Yatırma Başarısız',
|
||||
depositInstructions:
|
||||
'Aşağıdaki platform cüzdan adresine token gönderin, ardından doğrulamak için işlem hash yapıştırın.',
|
||||
'P2P bakiyenize yatırmak için bir token seçin ve miktarı girin. İşlem cüzdanınızdan otomatik olarak gönderilecektir.',
|
||||
depositInvalidAmount: 'Geçerli bir miktar girin',
|
||||
depositSending: 'İşlem gönderiliyor...',
|
||||
depositSendingDesc:
|
||||
"Yatırma işleminiz Asset Hub'a imzalanıp gönderiliyor. Bu işlem 30 saniyeye kadar sürebilir.",
|
||||
walletNotConnected: 'Cüzdan bağlı değil',
|
||||
connectWalletFirst: 'Yatırma yapmak için cüzdanınızın kilidini açın.',
|
||||
selectToken: 'Token Seçin',
|
||||
withdrawAmount: 'Çekim Miktarı',
|
||||
networkFee: 'Ağ Ücreti',
|
||||
|
||||
@@ -421,6 +421,10 @@ export interface Translations {
|
||||
depositFailed: string;
|
||||
depositInstructions: string;
|
||||
depositInvalidAmount: string;
|
||||
depositSending: string;
|
||||
depositSendingDesc: string;
|
||||
walletNotConnected: string;
|
||||
connectWalletFirst: string;
|
||||
selectToken: string;
|
||||
withdrawAmount: string;
|
||||
networkFee: string;
|
||||
|
||||
@@ -126,7 +126,7 @@ serve(async (req) => {
|
||||
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
|
||||
const {
|
||||
data: { users: authUsers },
|
||||
} = await supabase.auth.admin.listUsers();
|
||||
} = await supabase.auth.admin.listUsers({ perPage: 1000 });
|
||||
const authUser = authUsers?.find((u) => u.email === telegramEmail);
|
||||
|
||||
if (!authUser) {
|
||||
|
||||
@@ -138,7 +138,7 @@ serve(async (req) => {
|
||||
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
|
||||
const {
|
||||
data: { users: authUsers },
|
||||
} = await supabase.auth.admin.listUsers();
|
||||
} = await supabase.auth.admin.listUsers({ perPage: 1000 });
|
||||
const authUser = authUsers?.find((u) => u.email === telegramEmail);
|
||||
|
||||
if (!authUser) {
|
||||
|
||||
@@ -148,7 +148,7 @@ serve(async (req) => {
|
||||
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
|
||||
const {
|
||||
data: { users: existingUsers },
|
||||
} = await serviceClient.auth.admin.listUsers();
|
||||
} = await serviceClient.auth.admin.listUsers({ perPage: 1000 });
|
||||
const authUser = existingUsers?.find((u) => u.email === telegramEmail);
|
||||
|
||||
if (!authUser) {
|
||||
|
||||
@@ -355,7 +355,7 @@ serve(async (req) => {
|
||||
// Try to get existing auth user
|
||||
const {
|
||||
data: { users: existingUsers },
|
||||
} = await serviceClient.auth.admin.listUsers();
|
||||
} = await serviceClient.auth.admin.listUsers({ perPage: 1000 });
|
||||
let authUser = existingUsers?.find((u) => u.email === telegramEmail);
|
||||
|
||||
if (!authUser) {
|
||||
|
||||
Reference in New Issue
Block a user