diff --git a/src/assets/images/bitcoin-to-slot.png b/src/assets/images/bitcoin-to-slot.png new file mode 100644 index 00000000..e89345b2 Binary files /dev/null and b/src/assets/images/bitcoin-to-slot.png differ diff --git a/src/assets/translations/de.json b/src/assets/translations/de.json index db7a4d5b..d266ac57 100644 --- a/src/assets/translations/de.json +++ b/src/assets/translations/de.json @@ -119,6 +119,8 @@ "status": "Status", "timeLeft": "Verbleibende Zeit", "amount": "Betrag", + "alreadyPaid": "Bereits bezahlt", + "payTheRest": "Bezahlen Sie den Rest ({{sats}} sats) über Lightning oder Onchain", "paidOn": "Bezahlt am", "awaitingPayment": "Erwartete Zahlung", "returningToTerminal": "Rückkehr zum Terminal", diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json index 66caeb38..db14cf4e 100644 --- a/src/assets/translations/en.json +++ b/src/assets/translations/en.json @@ -120,6 +120,8 @@ "status": "Status", "timeLeft": "Time left", "amount": "Amount", + "alreadyPaid": "Already paid", + "payTheRest": "Pay the rest ({{sats}} sats) via Lightning or Onchain", "paidOn": "Paid on", "awaitingPayment": "Awaiting payment", "returningToTerminal": "Returning to terminal", diff --git a/src/assets/translations/es.json b/src/assets/translations/es.json index a323168b..75505c2b 100644 --- a/src/assets/translations/es.json +++ b/src/assets/translations/es.json @@ -119,6 +119,8 @@ "status": "Estado", "timeLeft": "Tiempo restante", "amount": "Importe", + "alreadyPaid": "Ya pagado", + "payTheRest": "Pagar el resto ({{sats}} sats) a través de Lightning o Onchain", "paidOn": "Pagado el", "awaitingPayment": "En espera de pago", "returningToTerminal": "Volver al terminal", diff --git a/src/assets/translations/fr.json b/src/assets/translations/fr.json index f58f1b98..c988c024 100644 --- a/src/assets/translations/fr.json +++ b/src/assets/translations/fr.json @@ -120,6 +120,8 @@ "status": "Statut", "timeLeft": "Temps restant", "amount": "Montant", + "alreadyPaid": "Déjà payé", + "payTheRest": "Payez le reste ({{sats}} sats) via Lightning ou Onchain", "paidOn": "Payé le", "awaitingPayment": "En attente de paiement", "returningToTerminal": "Retour au terminal", diff --git a/src/assets/translations/it.json b/src/assets/translations/it.json index e5846767..85c6de8a 100644 --- a/src/assets/translations/it.json +++ b/src/assets/translations/it.json @@ -119,6 +119,8 @@ "status": "Stato", "timeLeft": "Tempo rimanente", "amount": "Importo", + "alreadyPaid": "Già pagato", + "payTheRest": "Pagare il resto ({{sats}} sats) tramite Lightning o Onchain", "paidOn": "Pagato il", "awaitingPayment": "In attesa di pagamento", "returningToTerminal": "Ritorno al terminale", diff --git a/src/assets/translations/pt.json b/src/assets/translations/pt.json index e2ee231f..eab51985 100644 --- a/src/assets/translations/pt.json +++ b/src/assets/translations/pt.json @@ -119,6 +119,8 @@ "status": "Estado", "timeLeft": "Tempo restante", "amount": "Montante", + "alreadyPaid": "Já pago", + "payTheRest": "Pagar o restante ({{sats}} sats) via Lightning ou Onchain", "paidOn": "Pago em", "awaitingPayment": "A aguardar pagamento", "returningToTerminal": "Regresso ao terminal", diff --git a/src/screens/Invoice/Invoice.tsx b/src/screens/Invoice/Invoice.tsx index 9ef2dee9..c0a6ae35 100644 --- a/src/screens/Invoice/Invoice.tsx +++ b/src/screens/Invoice/Invoice.tsx @@ -62,14 +62,12 @@ import { import LottieView from "lottie-react-native"; import * as S from "./styled"; import { XOR } from "ts-essentials"; +import { numberWithSpaces } from "@utils/numberWithSpaces"; const PAID_ANIMATION_DURATION = 350; const getTrue = () => true; -const numberWithSpaces = (nb: number) => - nb.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, " "); - const { isWeb, isIos } = platform; type Status = @@ -87,6 +85,7 @@ type OnchainTx = { network: "onchain"; address: string; txId?: string; + amount?: number; vout_index?: number; confirmations?: number; minConfirmations?: number; @@ -198,7 +197,7 @@ export const Invoice = () => { const [paidAt, setPaidAt] = useState(); // Onchain data - const [onChainTx, setOnchainTx] = useState(); + const [onChainTxs, setOnchainTxs] = useState(); const isAlive = useMemo( () => !["settled", "expired", "unconfirmed"].includes(status), @@ -330,7 +329,7 @@ export const Invoice = () => { ); const updateInvoice = useCallback( - async (getInvoiceData: InvoiceType, isInitialData?: boolean) => { + (getInvoiceData: InvoiceType, isInitialData?: boolean) => { try { const _pr = getInvoiceData.paymentDetails.find( @@ -339,11 +338,9 @@ export const Invoice = () => { getInvoiceData.paymentDetails.find((p) => p.network === "lightning") ?.paymentRequest; - const _onChainData = - getInvoiceData.paymentDetails.find( - (p) => p.network === "onchain" && p.paidAt - ) || - getInvoiceData.paymentDetails.find((p) => p.network === "onchain"); + const unpaidOnchain = getInvoiceData.paymentDetails.find( + (p) => p.network === "onchain" && !p.paidAt + ); const _paymentMethod = getInvoiceData.paymentDetails.find( (p) => p.paidAt @@ -355,13 +352,17 @@ export const Invoice = () => { setDelay(getInvoiceData.expiry - getInvoiceData.time); setPr(_pr); setReadingNfcData(_pr); - setOnChainAddr(_onChainData?.address); + setOnChainAddr(unpaidOnchain?.address); setAmount(getInvoiceData.amount * 1000); setInvoiceCurrency(getInvoiceData.input.unit || "CHF"); setInvoiceFiatAmount(getInvoiceData.input.amount); setStatus(getInvoiceData.status); setPaymentMethod(_paymentMethod); - setOnchainTx(_onChainData); + setOnchainTxs( + getInvoiceData.paymentDetails.filter( + (p) => p.network === "onchain" && p.paidAt + ) + ); setPaidAt(getInvoiceData.paidAt); setIsInit(true); @@ -387,24 +388,6 @@ export const Invoice = () => { if (status === "expired") { return; } - - if (_paymentMethod === "onchain") { - try { - const { data: txDetails } = await axios.get( - `https://mempool.space/api/tx/${_onChainData?.txId}` - ); - if (txDetails.status.confirmed) { - const { data: blockHeight } = await axios.get( - "https://mempool.space/api/blocks/tip/height" - ); - setOnchainTx({ - ..._onChainData, - minConfirmations: - blockHeight - txDetails.status.block_height + 1 - }); - } - } catch (e) {} - } } catch (e) { setIsInvalidInvoice(true); return; @@ -478,6 +461,20 @@ export const Invoice = () => { [status, isExternalInvoice] ); + const alreadyPaidAmount = useMemo( + () => onChainTxs?.reduce((result, o) => result + (o.amount || 0), 0) || 0, + [onChainTxs] + ); + + const { confirmations, minConfirmations } = useMemo( + () => + (onChainTxs || []).reduce( + (result, o) => (o.confirmations < result.confirmations ? o : result), + { confirmations: 100, minConfirmations: 0 } + ), + [onChainTxs] + ); + const [isQrModalOpen, setIsQrModalOpen] = useState(false); const onOpenQrModal = useCallback(() => { @@ -635,7 +632,7 @@ export const Invoice = () => { /> ) : null} {!isAlive && ( - + {status === "unconfirmed" && ( )} @@ -741,6 +738,28 @@ export const Invoice = () => { {amount ? numberWithSpaces(amount / 1000) : ""} sats )} + {alreadyPaidAmount > 0 && status === "underpaid" && ( + <> + + + {t("alreadyPaid")}: {numberWithSpaces(alreadyPaidAmount)}{" "} + sats + + + {t("payTheRest", { + sats: numberWithSpaces( + amount / 1000 - alreadyPaidAmount + ) + })} + + + )} {status === "settled" && isExternalInvoice && @@ -868,26 +887,32 @@ export const Invoice = () => { .toString()} /> )} - {onChainTx?.txId && ( - - )} - {onChainTx?.confirmations !== undefined && ( - - )} + {onChainTxs?.map((tx) => { + const success = tx.confirmations >= tx.minConfirmations; + const color = success ? colors.success : colors.warning; + + return ( + <> + {tx.txId && ( + + })} + url={`https://mempool.space/tx/${tx.txId}${ + tx.vout_index !== undefined + ? `#vout=${tx.vout_index}` + : "" + }`} + value={truncate(tx.txId, 16)} + /> + )} + + ); + })} )} diff --git a/src/screens/Invoice/components/FooterLine/FooterLine.tsx b/src/screens/Invoice/components/FooterLine/FooterLine.tsx index c967285e..3c136e03 100644 --- a/src/screens/Invoice/components/FooterLine/FooterLine.tsx +++ b/src/screens/Invoice/components/FooterLine/FooterLine.tsx @@ -60,13 +60,6 @@ export const FooterLine = ({ target="_blank" rel="noopener noreferrer" > - {(copyable || url) && ( - - )} {prefixIcon && ( )} {prefixComponent && cloneElement(prefixComponent, { color })} + {(copyable || url) && ( + + )} {value} {valueSuffix} diff --git a/src/screens/Invoice/styled.tsx b/src/screens/Invoice/styled.tsx index ba2b2e0c..2a61ac97 100644 --- a/src/screens/Invoice/styled.tsx +++ b/src/screens/Invoice/styled.tsx @@ -15,7 +15,7 @@ import { import { platform } from "@config"; import { Circle } from "react-native-progress"; -const isNative = { platform }; +const { isNative } = platform; type InvoicePageContainerProps = { isLoading: boolean }; @@ -173,16 +173,28 @@ export const NFCSwitchContainerCircle = styled(View)` type AmountTextProps = { subAmount?: boolean; + color?: string; }; export const AmountText = styled(Text).attrs( - ({ theme, subAmount }) => ({ + ({ theme, subAmount, color }) => ({ ...(subAmount ? { h4: true } : { h2: true }), weight: 700, - color: theme.colors.white + color: color || theme.colors.white }) )``; +export const BitcoinSlotText = styled(AmountText)` + display: flex; + margin-top: 16px; +`; + +export const BitcoinSlotImage = styled(Image)` + width: 20px; + height: 20px; + margin-right: 6px; +`; + export const ProgressBar = styled(RootProgressBar)` width: 90%; `; diff --git a/src/utils/getFormattedUnit.ts b/src/utils/getFormattedUnit.ts index f0386a14..148c59ed 100644 --- a/src/utils/getFormattedUnit.ts +++ b/src/utils/getFormattedUnit.ts @@ -1,3 +1,5 @@ +import { numberWithSpaces } from "@utils"; + export const getFormattedUnit = ( amount: number, unit: string, @@ -10,7 +12,7 @@ export const getFormattedUnit = ( } if (unit === "sat" || unit === "sats") { - return `${prefix}${amount} sats`; + return `${prefix}${numberWithSpaces(amount)} sats`; } return `${prefix}${Intl.NumberFormat(undefined, { diff --git a/src/utils/index.ts b/src/utils/index.ts index 1b6d9efa..630d4ed7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -18,4 +18,5 @@ export { generateBtcAddress } from "./generateBtcAddress"; export { validateBitcoinAddress } from "./validateBitcoinAddress"; export { hexToRgb } from "./hexToRgb"; export { mergeDeep } from "./mergeDeep"; -export { isMinUserType } from "./isMinUserType"; \ No newline at end of file +export { isMinUserType } from "./isMinUserType"; +export { numberWithSpaces } from "./numberWithSpaces"; \ No newline at end of file diff --git a/src/utils/numberWithSpaces.ts b/src/utils/numberWithSpaces.ts new file mode 100644 index 00000000..17c7c68b --- /dev/null +++ b/src/utils/numberWithSpaces.ts @@ -0,0 +1,2 @@ +export const numberWithSpaces = (nb: number) => + nb.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, " ");