diff --git a/components/QrGenerator.tsx b/components/QrGenerator.tsx index 67cb2a10..f4ffcf24 100644 --- a/components/QrGenerator.tsx +++ b/components/QrGenerator.tsx @@ -78,6 +78,8 @@ export function QrGenerator() { const [step2Initialized, setStep2Initialized] = useState(false); // QR は即時表示せず「QRコードを表示する」ボタン → 全画面モーダルで提示。 const [qrModalOpen, setQrModalOpen] = useState(false); + // onClose は modal の focus effect の依存。残高更新や入力で再 focus させない。 + const closeQrModal = useCallback(() => setQrModalOpen(false), []); // 初回に QR モーダルを開いた「ピークモーメント」を latch。以降 A2HS hint を出す // (毎日この QR を使う店主に、ホーム画面への追加を提案する)。閉じても latch は保持。 const [hasOpenedQr, setHasOpenedQr] = useState(false); @@ -231,6 +233,7 @@ export function QrGenerator() { // 旧 QR との互換性を最大化するため token=usdc 時のみ出力する。 crossChain: settings.token === 'usdc' ? crossChainAllowed(settings.chain, settings.crossChain) : undefined, // convert 適用時のみ、期限と顧客への文脈表示 (元価格 + レート) を URL に乗せる。 + // 期限は画面上の目安。期限切れでも QR / exp は保持し、明示的な再計算で更新する。 expiresAt: convert?.expiresAt, priceRefAmount: convert?.anchorAmount, fxRate: convert?.fxRate, @@ -594,7 +597,7 @@ export function QrGenerator() { {payUrl && ( setQrModalOpen(false)} + onClose={closeQrModal} labels={{ title: t('qrModalTitle'), close: t('qrModalClose'), diff --git a/components/qr/QrAmountSection.tsx b/components/qr/QrAmountSection.tsx index e738ffd8..0d9c8d76 100644 --- a/components/qr/QrAmountSection.tsx +++ b/components/qr/QrAmountSection.tsx @@ -2,10 +2,12 @@ import { useMemo, + useRef, type ComponentProps, type Dispatch, type SetStateAction, } from 'react'; +import { flushSync } from 'react-dom'; import { useTranslations } from 'next-intl'; import { Coins } from 'lucide-react'; import { RecoverFeeNotice } from '../RecoverFeeNotice'; @@ -82,6 +84,7 @@ export function QrAmountSection({ recoverGasMode: GasMode; }) { const t = useTranslations('QrGenerator'); + const amountInputRef = useRef(null); // クイック金額は token (JPYC=円 / USDC=ドル) ごとに独立。エディタ・適用とも // 現在の token のサブリストだけを操作する。 @@ -164,7 +167,13 @@ export function QrAmountSection({ key={m} type="button" onClick={() => { - setMode(m as Mode); + if (m === 'amount' && mode === 'static') { + // iOS のキーボードを開けるよう、表示を反映してからタップ中に focus する。 + flushSync(() => setMode('amount')); + amountInputRef.current?.focus(); + } else { + setMode(m); + } resetConvert(); }} className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition ${ @@ -177,59 +186,60 @@ export function QrAmountSection({ ))} - {mode === 'amount' ? ( -
- {/* 金額ヒーロー: 入力欄を「表示器」化して数字を主役に。通貨記号は控えめな - 接尾、その真下に参考円 (USDC のみ・JPYC は ¥ ペッグで冗長ゆえ非表示)。 - 枠線は container 側に寄せ、focus で brand + 浮き影。 */} -
-
- { - setAmount( - truncateAmount(e.target.value, deployment.decimals), - ); - resetConvert(); - }} - placeholder={settings.token === 'jpyc' ? '1000' : '10.00'} - aria-label={t('amountLabel', { - symbol: deployment.displaySymbol, - })} - className="min-w-0 flex-1 bg-transparent text-right text-4xl font-bold tabular-nums tracking-tight text-slate-900 placeholder:text-slate-300 focus:outline-none sm:text-5xl" - autoFocus - /> - - {deployment.displaySymbol} - -
- {fiatHint && ( -
- {fiatHint} -
- )} + {/* 非表示でも node を保持し、再表示で入力欄を作り直さない。 */} +