WebberUI

Invoice Lottery Checker (React)

A React component for Taiwan's uniform-invoice lottery: type the last 3 or all 8 digits of an invoice number and match it against the prize numbers you pass in — confetti, a rolling prize amount and a prize badge on a win, a gentle head-shake and a one-tap check-the-next-one button on a miss.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

Taiwan's uniform-invoice lottery draws every two months, and checking receipts is a national pastime. This component packages "period title + 8-cell digit input + result panel" into a self-contained checker: the winning numbers come in through prizes, and the built-in matchInvoice() applies the Ministry of Finance's published prize rules to decide the special prize, grand prize, the first-prize family (last 8 down to last 3 digits) and the additional sixth prize. It also supports a quick last-3-digits mode (which can only settle the sixth / additional sixth prize and prompts for the full number to confirm anything higher). On a win, confetti particles burst, the prize amount rolls up and a prize badge appears; on a miss you get a gentle head-shake and a "check the next one" button. The digit cells are OTP-style: auto-advance, paste distribution, inputMode="numeric", Backspace to step back.

Loading preview…
npx shadcn@latest add "https://webberui.com/r/invoice-lottery-checker.json?t=<install token>"

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<InvoiceLotteryChecker />

Installation

npx shadcn@latest add "https://webberui.com/r/invoice-lottery-checker.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/invoice-lottery-checker.

Usage

import {
  InvoiceLotteryChecker,
  matchInvoice,
} from "@/components/ui/invoice-lottery-checker";

// Sample prize numbers, not a real draw — feed real ones from your backend or the Ministry of Finance open data in production
const prizes = {
  special: "13579246",
  grand: "24680135",
  first: ["11223344", "55667788", "99001122"],
  additionalSixth: ["357", "802"],
};

<InvoiceLotteryChecker
  prizes={prizes}
  period="115 年 5–6 月"
  onResult={(result, number) =>
    console.log(number, result.prize, result.amount)
  }
  note="示範獎號,非真實開獎結果"
/>

// The matcher can be used on its own (for example to check a whole stack of receipts in a batch)
matchInvoice("10203788", prizes); // { prize: "sixth", amount: 200, matchedDigits: 3, partial: false }
matchInvoice("788", prizes);      // { prize: "sixth", amount: 200, matchedDigits: 3, partial: true }

Props

PropTypeDefaultDescription
prizesInvoicePrizesThis period's winning numbers (special, grand, three first-prize numbers, additional sixth)
periodstring"本期統一發票" (this period's uniform-invoice draw)Period title, e.g. "115 年 5–6 月" (May–June of ROC year 115)
mode"full" | "last3""full"Checking mode: full takes all 8 digits; last3 takes only the last 3 (can only settle the sixth / additional sixth prize)
valuestringControlled value (digits only); when omitted the component is uncontrolled
defaultValuestring""Initial value in uncontrolled mode
onChange(value: string) => voidCallback when the value changes; the argument is the cleaned digits-only string
onResult(result: InvoiceMatchResult, number: string) => voidCallback every time a check completes (automatic or manual)
autoCheckbooleantrueCheck automatically once all cells are filled; when false the user presses the "對獎" (check) button or Enter
showConfettibooleantrueWhether to burst confetti on a win
showPrizesbooleantrueWhether to list this period's winning numbers below the input
notestringFootnote text, e.g. "示範獎號,非真實開獎結果" (sample numbers, not a real draw)
classNamestringForwarded to the outermost container

InvoicePrizes

FieldTypeDefaultDescription
specialstringSpecial prize number (8 digits)
grandstringGrand prize number (8 digits)
firststring[]First prize numbers (8 digits × 3)
additionalSixthstring[]Additional sixth prize numbers (3 digits, any count)

InvoiceMatchResult

FieldTypeDefaultDescription
prizeInvoicePrizeTierWinning tier: "special", "grand", "first", "second", "third", "fourth", "fifth", "sixth", "additionalSixth"; null when nothing was won
amountnumberPrize amount in NT$; 0 when nothing was won
matchedDigitsnumberHow many trailing digits matched the winning number; 0 when nothing was won
partialbooleanWhether only the last 3 digits were compared (fewer than 8 digits given); when true, higher tiers are still unconfirmed

matchInvoice(number, prizes), sanitizeInvoiceNumber(raw, maxLength?), INVOICE_PRIZE_LABELS (Chinese prize names), INVOICE_PRIZE_AMOUNTS (amount per tier) and the InvoicePrizeTier type are also named exports, so the same rules can be reused at the form layer or for batch checking.

How it works

  • Prize rules: special prize, all 8 digits, NT$10,000,000; grand prize, all 8 digits, NT$2,000,000; any of the three first-prize numbers — all 8 digits NT$200,000, last 7 NT$40,000, last 6 NT$10,000, last 5 NT$4,000, last 4 NT$1,000, last 3 NT$200 (sixth prize); additional sixth prize, last 3 digits, NT$200. When several tiers apply at once, the highest wins
  • With only 3 digits (mode="last3"), only the sixth / additional sixth prize can be settled; the win panel switches to "末三碼相符,請輸入完整號碼確認更高獎項" (last three digits match — enter the full number to confirm a higher prize) and onResult receives partial: true
  • The result panel is shown only while the number still equals the one that was checked: as soon as the user hits Backspace it collapses; under autoCheck a given complete number is checked once, and only re-checked after being edited and completed again
  • The confetti is implemented in-house with small motion squares and depends on no other component; every piece's landing spot, rotation and size come from a deterministic pseudo-random function, so Math.random is never called during render and SSR and CSR output match
  • In 8-digit mode the last three cells are set slightly apart and the last three digits of each first-prize number are bolded in the list — that is what decides the sixth prize, so it can be checked at a glance
  • No real winning numbers are built in: in production, feed prizes from your backend or the Ministry of Finance open data; the demo and playground use fictional numbers only and label them via note

Accessibility

  • The real input is carried by a single native input (inputMode="numeric", pattern="[0-9]*", maxLength); the visual cells are presentational only and marked aria-hidden, so keyboard behavior and the mobile numeric keypad behave exactly like an ordinary input; the label is bound with htmlFor
  • The result lives in a role="status" + aria-live="polite" region, so the tier, amount and hint are read out by screen readers; the input points at that region through aria-describedby
  • The "對獎" (check) and "再對下一張" (check the next one) buttons are type="button" with focus-visible rings; Enter triggers a manual check, and Enter pressed mid-composition in a Chinese IME is ignored (isComposing plus Safari's keyCode 229)
  • When the user has "reduce motion" enabled at the system level, the confetti, rolling amount, head-shake, cell pop-in and caret blink animations are disabled and the final result and text are shown directly

On this page