Blocks / commerce
Plan card
Presents a subscription plan to pick among others, as a radio button: name, billed price and period, trial, perks, and the recommended and current plan marks.
Import
Plan card is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:
fliin login
fliin add plan-cardExamples
Selected
Plans as a radio group: FlRadioGroup holds the choice and each FlPlanCard is one option, with its price, period and benefits. Tap a card to choose it.
import 'package:your_app/ui/fliin/components/plan_card/fl_plan_card.dart';
import 'package:your_app/ui/fliin/components/radio/fl_radio.dart';
import 'package:flutter/widgets.dart';
class PlanCardSelectedExample extends StatefulWidget {
const PlanCardSelectedExample({super.key});
@override
State<PlanCardSelectedExample> createState() => _PlanCardSelectedExampleState();
}
class _PlanCardSelectedExampleState extends State<PlanCardSelectedExample> {
var _chosen = 'pro';
@override
Widget build(BuildContext context) {
return FlRadioGroup<String>(
groupValue: _chosen,
onChanged: (value) => setState(() => _chosen = value ?? _chosen),
children: const [
FlPlanCard<String>(
value: 'plus',
name: 'Plus',
price: '€49.99',
period: 'per year',
priceDetail: 'that’s €4.17 a month',
benefits: ['Sync across 3 devices', '30-day history'],
),
FlPlanCard<String>(
value: 'pro',
name: 'Pro',
price: '€99.99',
period: 'per year',
priceDetail: '7 days free, then €99.99 a year',
benefits: ['Unlimited devices', 'PDF and CSV export'],
recommendedLabel: 'Recommended',
),
],
);
}
}Unselected
The same group with the other plan chosen: the card that is not selected stays readable, with a quieter border.
import 'package:your_app/ui/fliin/components/plan_card/fl_plan_card.dart';
import 'package:your_app/ui/fliin/components/radio/fl_radio.dart';
import 'package:flutter/widgets.dart';
class PlanCardUnselectedExample extends StatefulWidget {
const PlanCardUnselectedExample({super.key});
@override
State<PlanCardUnselectedExample> createState() => _PlanCardUnselectedExampleState();
}
class _PlanCardUnselectedExampleState extends State<PlanCardUnselectedExample> {
var _chosen = 'plus';
@override
Widget build(BuildContext context) {
return FlRadioGroup<String>(
groupValue: _chosen,
onChanged: (value) => setState(() => _chosen = value ?? _chosen),
children: const [
FlPlanCard<String>(
value: 'plus',
name: 'Plus',
price: '€49.99',
period: 'per year',
priceDetail: 'that’s €4.17 a month',
benefits: ['Sync across 3 devices', '30-day history'],
),
FlPlanCard<String>(
value: 'pro',
name: 'Pro',
price: '€99.99',
period: 'per year',
priceDetail: '7 days free, then €99.99 a year',
benefits: ['Unlimited devices', 'PDF and CSV export'],
recommendedLabel: 'Recommended',
),
],
);
}
}Recommended
recommendedLabel puts a badge on the plan you want to push; give it to one plan at most.
import 'package:your_app/ui/fliin/components/plan_card/fl_plan_card.dart';
import 'package:your_app/ui/fliin/components/radio/fl_radio.dart';
import 'package:flutter/widgets.dart';
class PlanCardRecommendedExample extends StatefulWidget {
const PlanCardRecommendedExample({super.key});
@override
State<PlanCardRecommendedExample> createState() => _PlanCardRecommendedExampleState();
}
class _PlanCardRecommendedExampleState extends State<PlanCardRecommendedExample> {
var _chosen = 'pro';
@override
Widget build(BuildContext context) {
return FlRadioGroup<String>(
groupValue: _chosen,
onChanged: (value) => setState(() => _chosen = value ?? _chosen),
children: const [
FlPlanCard<String>(
value: 'plus',
name: 'Plus',
price: '€49.99',
period: 'per year',
priceDetail: 'that’s €4.17 a month',
benefits: ['Sync across 3 devices', '30-day history'],
),
FlPlanCard<String>(
value: 'pro',
name: 'Pro',
price: '€99.99',
period: 'per year',
priceDetail: '7 days free, then €99.99 a year',
benefits: ['Unlimited devices', 'PDF and CSV export'],
recommendedLabel: 'Recommended',
),
],
);
}
}Current
currentLabel marks the plan the user already has, so choosing reads as a change rather than a purchase.
import 'package:your_app/ui/fliin/components/plan_card/fl_plan_card.dart';
import 'package:your_app/ui/fliin/components/radio/fl_radio.dart';
import 'package:flutter/widgets.dart';
class PlanCardCurrentExample extends StatefulWidget {
const PlanCardCurrentExample({super.key});
@override
State<PlanCardCurrentExample> createState() => _PlanCardCurrentExampleState();
}
class _PlanCardCurrentExampleState extends State<PlanCardCurrentExample> {
var _chosen = 'plus';
@override
Widget build(BuildContext context) {
return FlRadioGroup<String>(
groupValue: _chosen,
onChanged: (value) => setState(() => _chosen = value ?? _chosen),
children: const [
FlPlanCard<String>(
value: 'plus',
name: 'Plus',
price: '€49.99',
period: 'per year',
priceDetail: 'that’s €4.17 a month',
benefits: ['Sync across 3 devices', '30-day history'],
currentLabel: 'Current plan',
),
FlPlanCard<String>(
value: 'pro',
name: 'Pro',
price: '€99.99',
period: 'per year',
priceDetail: '7 days free, then €99.99 a year',
benefits: ['Unlimited devices', 'PDF and CSV export'],
recommendedLabel: 'Recommended',
),
],
);
}
}Compact
A paywall's plans on one line each with FlPlanCardLayout.compact: the name and its detail at the start, the price at the end. savingsLabel says what the yearly plan saves.
import 'package:your_app/ui/fliin/components/plan_card/fl_plan_card.dart';
import 'package:your_app/ui/fliin/components/radio/fl_radio.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';
class PlanCardCompactExample extends StatefulWidget {
const PlanCardCompactExample({super.key});
@override
State<PlanCardCompactExample> createState() => _PlanCardCompactExampleState();
}
class _PlanCardCompactExampleState extends State<PlanCardCompactExample> {
var _chosen = 'yearly';
@override
Widget build(BuildContext context) {
final gap = SizedBox(height: FlScope.of(context).space.md);
return FlRadioGroup<String>(
groupValue: _chosen,
onChanged: (value) => setState(() => _chosen = value ?? _chosen),
children: [
const FlPlanCard<String>(
value: 'yearly',
layout: FlPlanCardLayout.compact,
name: 'Yearly',
price: '€59.99',
period: 'per year',
priceDetail: 'Just €5.00 a month',
savingsLabel: 'Save 17%',
),
gap,
const FlPlanCard<String>(
value: 'monthly',
layout: FlPlanCardLayout.compact,
name: 'Monthly',
price: '€5.99',
period: 'per month',
priceDetail: 'Cancel anytime',
),
],
);
}
}Usage
The code the reference images render:
Localizations(locale: Locale'en', delegates: const [DefaultWidgetsLocalizations.delegate], child: ColoredBox(color: FlScope.of(context).colors.surface, child: Padding(padding: EdgeInsetsDirectional.all(FlScope.of(context).space.lg), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, spacing: FlScope.of(context).space.xl, children: [FlRadioGroup<String>(groupValue: 'pro', onChanged: (_) {}, children: [FlPlanCard<String>(value: 'plus', name: 'Plus', price: (variant.dir == TextDirection.rtl ? "4٫99 €" : "€4.99"), period: (variant.dir == TextDirection.rtl ? "شهريًا" : "per month"), benefits: [(variant.dir == TextDirection.rtl ? "مزامنة على 3 أجهزة" : "Sync across 3 devices"), (variant.dir == TextDirection.rtl ? "سجل 30 يومًا" : "30-day history")], currentLabel: (variant.dir == TextDirection.rtl ? "خطتك الحالية" : "Current plan")), FlPlanCard<String>(value: 'pro', name: 'Pro', price: (variant.dir == TextDirection.rtl ? "99٫99 €" : "€99.99"), period: (variant.dir == TextDirection.rtl ? "سنويًا" : "per year"), priceDetail: (variant.dir == TextDirection.rtl ? "مجانًا لمدة 7 أيام، ثم 99٫99 € سنويًا\nأي 8٫33 € شهريًا" : "7 days free, then €99.99 a year\nthat’s €8.33 a month"), benefits: [(variant.dir == TextDirection.rtl ? "أجهزة غير محدودة" : "Unlimited devices"), (variant.dir == TextDirection.rtl ? "تصدير PDF وCSV" : "PDF and CSV export"), (variant.dir == TextDirection.rtl ? "دعم ذو أولوية" : "Priority support")], recommendedLabel: (variant.dir == TextDirection.rtl ? "موصى بها" : "Recommended"))])]))))API
FlPlanCardLayout: enum {full, compact}
FlPlanCardLayout.full: static final FlPlanCardLayout
FlPlanCardLayout.compact: static final FlPlanCardLayout
FlPlanCard: class<T> extends StatelessWidget
FlPlanCard.new: const new({super.key, required T value, required String name, required String price, required String period, String? priceDetail, String? description, List<String> benefits, String? recommendedLabel, String? currentLabel, bool enabled, FlPlanCardLayout layout, String? savingsLabel})
FlPlanCard.value: get T
FlPlanCard.name: get String
FlPlanCard.price: get String
FlPlanCard.period: get String
FlPlanCard.priceDetail: get String?
FlPlanCard.description: get String?
FlPlanCard.benefits: get List<String>
FlPlanCard.recommendedLabel: get String?
FlPlanCard.currentLabel: get String?
FlPlanCard.enabled: get bool
FlPlanCard.layout: get FlPlanCardLayout
FlPlanCard.savingsLabel: get String?
FlPlanCard.build: Widget(BuildContext context)Reference renders
The 16 images published with version 0.2.2, rendered by the shared harness on macOS with Flutter 3.47.7. fliin verify plan-card compares your copy to them.
















Use it to
- present a subscription tier with its price and perks
- make the recommended tier stand out among several
- make clear which tier the user already has
- show how long the free trial lasts and the price after it
- pick a pricing tier in a subscription sheet
- Not for
- a short choice without a price: use radio buttons (radio)
- the whole plans screen: use the plans screen (plans)
- States