bank_card_3d 0.1.1
bank_card_3d: ^0.1.1 copied to clipboard
Visa and Mastercard cards for Flutter in real 3D: photoreal or glass, every tier, a wallet stack, a live entry form and a minting animation. Zero dependencies, all six platforms.
bank_card_3d
Visa and Mastercard cards for Flutter, rendered in real 3D.
Drag them, flick them, flip them. Photoreal metal or frosted glass, every tier,
a wallet stack, a live entry form — straight from whatever JSON your backend sends.
Built by Mohammad Othman (Abojawdat)
Not a video, not an image asset. Every frame above is painted by the package at run time.
What you get #
- Real 3D — perspective, a card edge with thickness, a floor shadow, spring physics. Drag to tilt, flick to spin, double-tap to flip. On desktop and web the card leans toward the mouse; on phones, hand it the gyroscope.
- A minting animation — the card flies in, the chip drops into place, every digit is stamped one by one, the logo pops and a light sweeps across.
- Every tier, in its own material — Visa Classic, Gold, Platinum, Signature, Infinite; Mastercard Standard, Gold, Platinum, World, World Elite. Plastic, brushed foil and spun black metal, each catching light as it turns.
- Two looks —
CardLook.photoreal(embossed digits, EMV chip, magstripe, hologram) andCardLook.glass(frosted glass over drifting light). - Reads your API, however broken —
BankCard.fromJsonnever throws and never returns null: any key spelling or nesting, Stripe's{brand, last4, exp_month}, a bare"4242", a raw JSON string,13/99expiries,"null"names, Arabic digits… - Null-safe display — anything the backend didn't send is drawn as
*stars. No brand? You get a neutral card instead of nothing. - You choose what shows — full number, last 4, masked or hidden; holder, expiry and CVV shown, masked or hidden; chip, contactless, tier and DEBIT/CREDIT labels on or off.
- Wallet stack — many cards stacked like a phone wallet; they deal in, tap one to pull it out.
- Live entry form — the card fills in as the user types, the brand morphs in, the card flips over for the CVV. Luhn, length and expiry validation built in.
- English and Arabic — labels, validation messages, RTL, Eastern-Arabic digit input. Picked from the app locale.
- Fast — a card at rest is painted once and cached; each frame only moves the light over it.
- Zero dependencies. Flutter only, all six platforms.
Install #
flutter pub add bank_card_3d
import 'package:bank_card_3d/bank_card_3d.dart';
Quick start #
BankCard3D(
card: BankCard(
number: '4532 0151 1283 0366',
holderName: 'Mohammad Othman',
expiryMonth: 8,
expiryYear: 2029,
tier: CardTier.platinum,
),
)
That's all. The brand is detected from the number, the material from the tier. BankCard3D takes exactly width × width / kCardAspectRatio (ISO ID-1, 85.60 × 53.98 mm).
Straight from your API #
BankCard.fromJson takes whatever your backend sends and always gives you a card — it never throws and never returns null. Whatever is missing or broken is drawn as stars.
final cards = [for (final json in response['cards']) BankCard.fromJson(json)];
BankCardWallet(cards: cards)
All of these work:
{'card_number': '4532015112830366', 'holder_name': 'Mohammad Othman', 'expiry': '08/29', 'scheme': 'VISA'}
{'masked_pan': '542523******9903', 'cardholder_name': 'SARA KAREEM', 'exp_month': 11, 'exp_year': 2028}
{'card': {'brand': 'visa', 'last4': '4242', 'exp_month': 3, 'exp_year': 2030, 'funding': 'debit'}} // Stripe
{'data': {'payment_method': {'card': {'Brand': 'VISA_DEBIT', 'LAST4': '4242'}}}} // nested, any case
{'last4': '7731'} // only last4, no brand
'4242' // just a string
When the backend gets it wrong #
| The API sends | The card shows |
|---|---|
only last4, no brand |
a neutral graphite card, **** **** **** 7731, no logo |
no holder name, or "null", "N/A", "-" |
************ |
no expiry, 13/99, soon, year 1850 |
**/** |
last4: 42 as a number (zeros lost) |
0042 |
card_number: "4242" (really the last four) |
**** **** **** 4242, brand not guessed from it |
brand inside another field (product: "Visa Platinum", card_type: "MASTERCARD") |
the right logo and tier |
| Amex, Discover, Maestro… | a neutral card — only Visa and Mastercard get a logo |
null, [], "garbage", a raw JSON string |
a fully starred card / the parsed card — never a crash |
Keys are matched in any case and spelling (card_number, CardNumber, CARD-NUMBER), at any nesting depth — but a bank: {name: …} is never mistaken for the holder.
Building it yourself from just the last four:
BankCard.lastFour('4242') // brand unknown → neutral card
BankCard.lastFour('4242', brand: CardBrand.visa) // when you do know it
Need to drop records that have no number at all? BankCard.tryFromJson(json) returns null for those. toJson() writes a card back as snake_case and never includes the CVV.
Show only what you want #
BankCard3D(
card: card,
display: const CardDisplay(
number: NumberDisplay.lastFour, // full · lastFour · mask · hide
holder: FieldDisplay.hide, // show · mask · hide
expiry: FieldDisplay.show,
cvv: FieldDisplay.mask,
chip: true,
contactless: true,
tierName: true, // PLATINUM, World Elite…
kindName: false, // DEBIT, CREDIT, PREPAID
mask: '*', // or '•'
),
)
Ready-made: CardDisplay() (last four, CVV masked — the default), CardDisplay.private (last four only, nothing personal), CardDisplay.everything (all in the clear — what the form uses).
Missing data never breaks the card. No holder name? No expiry? Only last4, no brand? Those spots are drawn as stars instead of leaving holes.
Wallet #
BankCardWallet(
cards: cards,
onSelected: (index) => setState(() => picked = index), // null when put back
)
Cards are dealt in one by one. Tap a card to pull it out — it becomes a full interactive BankCard3D — tap it again to put it back. One card? You just get the card.
Entry form #
final form = GlobalKey<FormState>();
BankCard? card;
BankCardForm(
formKey: form,
onChanged: (c) => card = c,
)
FilledButton(
onPressed: () {
if (form.currentState!.validate()) save(card!);
},
child: const Text('Save card'),
)
Numbers group themselves as you type (Arabic digits welcome), the brand logo morphs in, expiry formats to MM/YY, focusing the CVV flips the card. Autofill hints are set, so the platform's saved cards work. Only Visa and Mastercard pass validation.
Looks and tiers #
| Tier | Visa | Mastercard | Material |
|---|---|---|---|
standard |
Classic | Standard | plastic |
gold |
Gold | Gold | brushed gold foil |
platinum |
Platinum | Platinum | brushed platinum |
signature |
Signature | — | brushed graphite |
infinite |
Infinite | — | black metal, champagne digits |
world |
— | World | plastic, wine |
worldElite |
— | World Elite | black metal |
Your own colours:
BankCard3D(card: card, skin: CardSkin.branded(const Color(0xFF00897B)))
BankCard3D(card: card, look: CardLook.glass)
Gyroscope #
The package stays dependency-free, so it takes the tilt as a stream (x and y in −1…1). With sensors_plus:
Offset? rest;
final tilt = accelerometerEventStream().map((e) {
final now = Offset(e.x, e.y);
rest = Offset.lerp(rest ?? now, now, 0.02); // slowly re-centres on how the phone is held
final d = (now - rest!) / 3;
return Offset(-d.dx, d.dy);
}).asBroadcastStream();
BankCard3D(card: card, tilt: tilt)
Small pieces #
BankCardView(card: card, width: 120) // flat, no animation — for lists
BankCardView(card: card, side: CardSide.back)
CardBrandMark(brand: CardBrand.mastercard) // just the logo
CardBrand.detect('5425 23') // CardBrand.mastercard, safe on every keystroke
BankCard(number: '4532 0151 1283 0367').numberIssue // CardIssue.failedChecksum
Arabic #
Under an Arabic locale every label and validation message switches to Arabic. Force it either way with labels: BankCardLabels.arabic / BankCardLabels.english, or bring another language with BankCardLabels.english.copyWith(...).
Example app #
example/ has the showcase (every brand, tier and display option), a wallet built from a mixed API response, the entry form, an English/Arabic switch and the gyroscope hookup. It's the same app as the live demo.
cd example && flutter run
How it's drawn #
Everything is CustomPainter in millimetres on the real ID-1 card. The digits are a hand-drawn embossed typeface (no font files); the chip, hologram, guilloche and brushed-metal textures are paths and gradients. The 3D is Matrix4 perspective with a stack of edge layers for the thickness. Once a card stops changing, its face is baked into an image, so a resting or floating card costs one image and a few gradients per frame.
Found a bug? Want something? #
Open an issue — there's a short form for bugs and one for ideas, in English or Arabic.
A good bug report has:
- the
bank_card_3dversion andflutter --version - the platform (Android, iOS, web, macOS, Windows, Linux)
- the widget code you used
- if the card came from your API, the JSON for that card — with real numbers, names and CVVs removed. A card that draws wrong from your backend is exactly what the parser should learn to read, so send the shape, not the data.
- a screenshot or screen recording
Pull requests are welcome too. Run flutter test and dart format . first; CI checks both.
Releasing #
Bump version: in pubspec.yaml, add a line to CHANGELOG.md, push to main. GitHub Actions tags vX.Y.Z and publishes to pub.dev; the live demo redeploys on every push to main.
Trademarks #
Visa and Mastercard names and marks belong to their owners. The package draws them only to show which network a card belongs to.
بالعربية #
بطاقات فيزا وماستركارد ثلاثية الأبعاد لتطبيقات Flutter.
اسحب البطاقة، ارمِها لتدور، أو انقر مرتين لقلبها. مظهر واقعي (أرقام بارزة، شريحة، شريط مغناطيسي، هولوغرام) أو مظهر زجاجي، بكل الفئات: كلاسيك، ذهبية، بلاتينية، سيغنتشر، إنفينت، وورلد، وورلد إيليت. بدون أي اعتماديات، على المنصات الست.
التثبيت #
flutter pub add bank_card_3d
من الـ API مباشرة #
fromJson تقرأ أي شكل يرسله الخادم (card_number و masked_pan و last4 و exp_month …) ولا ترمي استثناء ولا تُرجع null أبداً. إذا أرسل الخادم آخر ٤ أرقام فقط بدون نوع البطاقة، تظهر بطاقة محايدة بدون شعار والباقي نجوم.
final cards = [for (final json in response['cards']) BankCard.fromJson(json)];
BankCardWallet(cards: cards)
اختر ما يظهر #
BankCard3D(
card: card,
display: const CardDisplay(
number: NumberDisplay.lastFour, // آخر ٤ أرقام فقط
holder: FieldDisplay.hide, // إخفاء الاسم
cvv: FieldDisplay.mask, // رمز الأمان نجوم
),
)
أي معلومة لم يرسلها الخادم (الاسم، تاريخ الانتهاء…) تُرسم نجوماً * بدل أن تترك فراغاً.
نموذج الإدخال #
BankCardForm تملأ البطاقة أثناء الكتابة، وتكتشف نوعها، وتقلبها عند كتابة رمز الأمان، وتتحقق من الرقم (Luhn) والتاريخ. تقبل الأرقام العربية، والرسائل بالعربية تلقائياً تحت اللغة العربية.
وجدت مشكلة؟ #
افتح issue بالعربية أو الإنجليزية. اذكر رقم إصدار الحزمة، والمنصة، والكود الذي استخدمته، وإذا كانت البطاقة قادمة من الـ API أرسل شكل الـ JSON بعد حذف أرقام البطاقات والأسماء ورمز الأمان الحقيقية.
صنعها محمد عثمان (Abojawdat).
Author #
Built by Mohammad Othman (Abojawdat) — github.com/Abojawdat. If it saved you a week of CustomPainter, a ⭐ is appreciated.
Licence #
MIT — see LICENSE.
