Screens / commerce
Plans and subscription
Offers subscription plans with a monthly or yearly switch, plan cards, the current plan, a free trial, in-app purchase through the store, restore purchases and the terms.
Import
Plans and subscription 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 plansAlso installs: alert, app-bar, button, core, empty-state, icons, link-button, page, plan-card, radio, segmented, skeleton, text
Examples
Ready
The plans loaded, the recommended one chosen. standalone holds its own controller over the service you pass; give it yours, wired to your store.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansReadyExample extends StatelessWidget {
const PlansReadyExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: PlansReady.initial(samplePlanCatalog),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onManageSubscription: () {},
onSubscribed: (_) {},
);
}
}Loading
While the catalog loads, placeholders keep the place of the plans.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansLoadingExample extends StatelessWidget {
const PlansLoadingExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: const PlansLoading(),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onManageSubscription: () {},
onSubscribed: (_) {},
);
}
}Purchasing
While the store confirms the purchase, the button waits and the choice is locked.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansPurchasingExample extends StatelessWidget {
const PlansPurchasingExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: const PlansReady(samplePlanCatalog, period: PlanPeriod.yearly, planId: 'pro', busy: PlansBusy.purchasing),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onManageSubscription: () {},
onSubscribed: (_) {},
);
}
}Purchased
Once subscribed, the screen thanks the user and offers to manage the subscription; onSubscribed tells your app.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansPurchasedExample extends StatelessWidget {
const PlansPurchasedExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: const PlansSubscribed(PlanSubscription('pro', PlanPeriod.yearly), planName: 'Pro'),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onManageSubscription: () {},
onSubscribed: (_) {},
);
}
}Failed
When the store does not confirm, a notice says so and the user can try again; nothing was charged twice.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansFailedExample extends StatelessWidget {
const PlansFailedExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: const PlansReady(samplePlanCatalog, period: PlanPeriod.yearly, planId: 'pro', notice: PlansNotice.purchaseUnconfirmed),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onManageSubscription: () {},
onSubscribed: (_) {},
);
}
}Error
When the catalog cannot load, the screen says so and offers to try again.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansErrorExample extends StatelessWidget {
const PlansErrorExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: const PlansFailed(),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onManageSubscription: () {},
onSubscribed: (_) {},
);
}
}Offline
Without a connection, the screen asks to reconnect.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansOfflineExample extends StatelessWidget {
const PlansOfflineExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: const PlansOffline(),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onManageSubscription: () {},
onSubscribed: (_) {},
);
}
}Header
The plans under a head of your own, like a paywall: a mark, a headline and what the plan unlocks. header takes any widget, and title replaces the bar's.
import 'package:your_app/ui/fliin/components/text/fl_text.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansHeaderExample extends StatelessWidget {
const PlansHeaderExample({super.key});
@override
Widget build(BuildContext context) {
return PlansScreen.standalone(
service: const SamplePlansService(latency: Duration(milliseconds: 600)),
initial: PlansReady.initial(samplePlanCatalog),
title: 'Go Pro',
header: const _Hero(),
onTerms: () {},
onPrivacy: () {},
onClose: () {},
onSubscribed: (_) {},
);
}
}
class _Hero extends StatelessWidget {
const _Hero();
@override
Widget build(BuildContext context) {
final theme = FlScope.of(context);
final space = theme.space;
return Column(
spacing: space.sm,
children: [
DecoratedBox(
decoration: ShapeDecoration(color: theme.colors.surfaceSecondary, shape: const CircleBorder()),
child: Padding(
padding: EdgeInsetsDirectional.all(space.md),
child: FlIcon(FlIcons.starFill, size: theme.size.buttonIconLg, color: theme.colors.accent),
),
),
const FlText('Unlock every feature', type: FlTextType.h3, textAlign: TextAlign.center),
const FlText(
'Unlimited devices, your full history and priority support. Cancel anytime.',
tone: FlTextTone.muted,
textAlign: TextAlign.center,
),
],
);
}
}Routing
fliin add plans prints the route for the router it finds in your pubspec, a file next to the page that wires its navigation. Each of them:
go_router
// lib/plans_route.dart, next to plans_page.dart.
import 'package:go_router/go_router.dart';
import 'plans_page.dart';
/// Add plansRoute to the routes of your GoRouter.
final plansRoute = GoRoute(
path: '/plans',
builder: (context, state) => PlansPage(
// The path of the subscription terms of your app.
onTerms: () => context.push('/terms'),
// The path of the privacy policy of your app.
onPrivacy: () => context.push('/privacy'),
// Alone in the stack (deep link, go): your app's home.
onClose: () => context.canPop() ? context.pop() : context.go('/'),
// Alone in the stack (deep link, go): your app's home.
onSubscribed: (_) => context.canPop() ? context.pop() : context.go('/'),
),
);navigator
// lib/plans_route.dart, next to plans_page.dart.
import 'package:flutter/material.dart';
import 'plans_page.dart';
/// Opens “Plans and subscription”: `Navigator.of(context).push(plansRoute(termsPage: …, privacyPage: …))`.
/// [termsPage] builds the subscription terms of your app.
/// [privacyPage] builds the privacy policy of your app.
Route<void> plansRoute({
required WidgetBuilder termsPage,
required WidgetBuilder privacyPage,
}) => MaterialPageRoute<void>(
builder: (context) => PlansPage(
onTerms: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: termsPage)),
onPrivacy: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: privacyPage)),
onClose: () => Navigator.of(context).maybePop(),
onSubscribed: (_) => Navigator.of(context).maybePop(),
),
);State
The controller is a ChangeNotifier from widgets.dart, with no package. fliin add plans prints the page that holds it with the state management it finds, its imports written for your package; below they read your_app. Your backend implements the Service interface of the screen's _data.dart file:
none
// lib/plans_page.dart
// Your backend: implement PlansService (plans_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
class PlansPage extends StatelessWidget {
const PlansPage({
super.key,
required this.onTerms,
required this.onPrivacy,
required this.onClose,
required this.onSubscribed,
});
final VoidCallback onTerms;
final VoidCallback onPrivacy;
final VoidCallback onClose;
final ValueChanged<PlanSubscription> onSubscribed;
@override
Widget build(BuildContext context) => PlansScreen.standalone(
service: const SamplePlansService(),
onTerms: onTerms,
onPrivacy: onPrivacy,
onClose: onClose,
onSubscribed: onSubscribed,
);
}provider
// lib/plans_page.dart
// Your backend: implement PlansService (plans_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class PlansPage extends StatelessWidget {
const PlansPage({
super.key,
required this.onTerms,
required this.onPrivacy,
required this.onClose,
required this.onSubscribed,
});
final VoidCallback onTerms;
final VoidCallback onPrivacy;
final VoidCallback onClose;
final ValueChanged<PlanSubscription> onSubscribed;
@override
Widget build(BuildContext context) => ChangeNotifierProvider(
create: (_) => PlansController(service: const SamplePlansService())..load(),
child: Builder(
builder: (context) => PlansScreen(
controller: context.read<PlansController>(),
onTerms: onTerms,
onPrivacy: onPrivacy,
onClose: onClose,
onSubscribed: onSubscribed,
),
),
);
}riverpod
// lib/plans_page.dart
// Your backend: implement PlansService (plans_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final plansControllerProvider = Provider.autoDispose<PlansController>((ref) {
final controller = PlansController(service: const SamplePlansService())..load();
ref.onDispose(controller.dispose);
return controller;
});
class PlansPage extends ConsumerWidget {
const PlansPage({
super.key,
required this.onTerms,
required this.onPrivacy,
required this.onClose,
required this.onSubscribed,
});
final VoidCallback onTerms;
final VoidCallback onPrivacy;
final VoidCallback onClose;
final ValueChanged<PlanSubscription> onSubscribed;
@override
Widget build(BuildContext context, WidgetRef ref) => PlansScreen(
controller: ref.watch(plansControllerProvider),
onTerms: onTerms,
onPrivacy: onPrivacy,
onClose: onClose,
onSubscribed: onSubscribed,
);
}bloc
// lib/plans_page.dart
// Your backend: implement PlansService (plans_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/plans/plans_controller.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_data.dart';
import 'package:your_app/ui/fliin/screens/plans/plans_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// The screen's controller as a bloc, for your BlocListener and BlocBuilder:
/// one emission per controller notification. `version` tells them apart
/// when `state` does not change (mode, filters, text…, read them on
/// `controller`). The screen listens to the controller itself.
class PlansCubit extends Cubit<({PlansState state, int version})> {
PlansCubit(this.controller) : super((state: controller.state, version: 0)) {
controller.addListener(_changed);
}
final PlansController controller;
void _changed() => emit((state: controller.state, version: state.version + 1));
@override
Future<void> close() {
controller
..removeListener(_changed)
..dispose();
return super.close();
}
}
class PlansPage extends StatelessWidget {
const PlansPage({
super.key,
required this.onTerms,
required this.onPrivacy,
required this.onClose,
required this.onSubscribed,
});
final VoidCallback onTerms;
final VoidCallback onPrivacy;
final VoidCallback onClose;
final ValueChanged<PlanSubscription> onSubscribed;
@override
Widget build(BuildContext context) => BlocProvider(
create: (_) => PlansCubit(PlansController(service: const SamplePlansService())..load()),
child: Builder(
builder: (context) => PlansScreen(
controller: context.read<PlansCubit>().controller,
onTerms: onTerms,
onPrivacy: onPrivacy,
onClose: onClose,
onSubscribed: onSubscribed,
),
),
);
}API
PlansScreen: class extends StatefulWidget
PlansScreen.new: const new({super.key, required PlansController controller, required VoidCallback onTerms, required VoidCallback onPrivacy, VoidCallback? onClose, ValueChanged<PlanSubscription>? onSubscribed, VoidCallback? onManageSubscription, String? title, Widget? header, bool showRenewal})
PlansScreen.standalone: const new({super.key, required PlansService service, required VoidCallback onTerms, required VoidCallback onPrivacy, PlansState? initial, VoidCallback? onClose, ValueChanged<PlanSubscription>? onSubscribed, VoidCallback? onManageSubscription, String? title, Widget? header, bool showRenewal})
PlansScreen.controller: get PlansController?
PlansScreen.service: get PlansService?
PlansScreen.initial: get PlansState?
PlansScreen.onTerms: get VoidCallback
PlansScreen.onPrivacy: get VoidCallback
PlansScreen.onClose: get VoidCallback?
PlansScreen.onSubscribed: get ValueChanged<PlanSubscription>?
PlansScreen.onManageSubscription: get VoidCallback?
PlansScreen.title: get String?
PlansScreen.header: get Widget?
PlansScreen.showRenewal: get bool
PlansScreen.createState: State<PlansScreen>()
extension String.nullIfEmpty: get String?
PlansState: sealed class permits {PlansFailed, PlansLoading, PlansOffline, PlansReady, PlansSubscribed}
PlansState.new: const new()
PlansLoading: final class extends PlansState
PlansLoading.new: const new()
PlansFailed: final class extends PlansState
PlansFailed.new: const new()
PlansOffline: final class extends PlansState
PlansOffline.new: const new()
PlansBusy: enum {purchasing, restoring}
PlansBusy.purchasing: static final PlansBusy
PlansBusy.restoring: static final PlansBusy
PlansNotice: enum {purchaseDeclined, purchaseUnconfirmed, purchaseOffline, pending, nothingToRestore, restoreFailed, restoreOffline}
PlansNotice.purchaseDeclined: static final PlansNotice
PlansNotice.purchaseUnconfirmed: static final PlansNotice
PlansNotice.purchaseOffline: static final PlansNotice
PlansNotice.pending: static final PlansNotice
PlansNotice.nothingToRestore: static final PlansNotice
PlansNotice.restoreFailed: static final PlansNotice
PlansNotice.restoreOffline: static final PlansNotice
PlansReady: final class extends PlansState
PlansReady.new: const new(PlanCatalog catalog, {required PlanPeriod period, required String planId, PlansBusy? busy, PlansNotice? notice, String? awaiting})
PlansReady.initial: factory new(PlanCatalog catalog)
PlansReady.catalog: get PlanCatalog
PlansReady.period: get PlanPeriod
PlansReady.planId: get String
PlansReady.busy: get PlansBusy?
PlansReady.notice: get PlansNotice?
PlansReady.awaiting: get String?
PlansReady.isAwaiting: get bool
PlansReady.offered: get List<Plan>
PlansReady.price: get PlanPrice?
PlansReady.isCurrent: get bool
PlansSubscribed: final class extends PlansState
PlansSubscribed.new: const new(PlanSubscription subscription, {required String? planName, bool restored})
PlansSubscribed.subscription: get PlanSubscription
PlansSubscribed.planName: get String?
PlansSubscribed.restored: get bool
extension PlanCatalog.periods: get List<PlanPeriod>
PlansController: class extends ChangeNotifier
PlansController.new: new({required PlansService service, PlansState initial})
PlansController.service: get PlansService
PlansController.state: get PlansState
PlansController.load: Future<bool>()
PlansController.setPeriod: bool(PlanPeriod period)
PlansController.choose: bool(String planId)
PlansController.purchase: Future<bool>()
PlansController.restore: Future<bool>()
PlansController.dispose: void()
PlanPeriod: enum {monthly, yearly}
PlanPeriod.monthly: static final PlanPeriod
PlanPeriod.yearly: static final PlanPeriod
PlanPrice: final class
PlanPrice.new: const new({required String productId, required String price, String? detail, String? trial})
PlanPrice.productId: get String
PlanPrice.price: get String
PlanPrice.detail: get String?
PlanPrice.trial: get String?
Plan: final class
Plan.new: const new({required String id, required String name, required Map<PlanPeriod, PlanPrice> prices, String? description, List<String> benefits, bool recommended})
Plan.id: get String
Plan.name: get String
Plan.prices: get Map<PlanPeriod, PlanPrice>
Plan.description: get String?
Plan.benefits: get List<String>
Plan.recommended: get bool
PlanSubscription: final class
PlanSubscription.new: const new(String planId, PlanPeriod period)
PlanSubscription.planId: get String
PlanSubscription.period: get PlanPeriod
PlanCatalog: final class
PlanCatalog.new: const new({required List<Plan> plans, PlanSubscription? current, Map<PlanPeriod, String> savings})
PlanCatalog.plans: get List<Plan>
PlanCatalog.current: get PlanSubscription?
PlanCatalog.savings: get Map<PlanPeriod, String>
PlanPurchaseResult: enum {purchased, pending, cancelled}
PlanPurchaseResult.purchased: static final PlanPurchaseResult
PlanPurchaseResult.pending: static final PlanPurchaseResult
PlanPurchaseResult.cancelled: static final PlanPurchaseResult
PlansNoNetwork: final class implements Exception
PlansNoNetwork.new: const new()
PlansPurchaseDeclined: final class implements Exception
PlansPurchaseDeclined.new: const new()
PlansService: abstract interface class
PlansService.new: new()
PlansService.load: abstract Future<PlanCatalog>()
PlansService.purchase: abstract Future<PlanPurchaseResult>(PlanPrice price)
PlansService.restore: abstract Future<PlanSubscription?>()
samplePlanCatalog: const dynamic
SamplePlansService: final class implements PlansService
SamplePlansService.new: const new({PlanCatalog catalog, Duration latency})
SamplePlansService.catalog: get PlanCatalog
SamplePlansService.latency: get Duration
SamplePlansService.load: Future<PlanCatalog>()
SamplePlansService.purchase: Future<PlanPurchaseResult>(PlanPrice price)
SamplePlansService.restore: Future<PlanSubscription?>()
plansL10n: const Map<String, Map<String, String>>
PlansStrings: final class
PlansStrings.new: const new(String language)
PlansStrings.of: factory new(BuildContext context)
PlansStrings.language: get String
PlansStrings.title: get String
PlansStrings.close: get String
PlansStrings.loading: get String
PlansStrings.period: get String
PlansStrings.monthly: get String
PlansStrings.yearly: get String
PlansStrings.withSavings: String(String period, String savings)
PlansStrings.perMonth: get String
PlansStrings.perYear: get String
PlansStrings.plans: get String
PlansStrings.recommended: get String
PlansStrings.current: get String
PlansStrings.subscribe: get String
PlansStrings.startTrial: get String
PlansStrings.currentAction: get String
PlansStrings.billing: String(String price, String period)
PlansStrings.renewal: get String
PlansStrings.terms: get String
PlansStrings.privacy: get String
PlansStrings.restore: get String
PlansStrings.restorePurchases: get String
PlansStrings.manage: get String
PlansStrings.purchaseDeclined: get String
PlansStrings.purchaseUnconfirmed: get String
PlansStrings.purchaseOffline: get String
PlansStrings.awaitingApproval: get String
PlansStrings.pending: get String
PlansStrings.nothingToRestore: get String
PlansStrings.restoreFailed: get String
PlansStrings.restoreOffline: get String
PlansStrings.subscribedTitle: String(String plan)
PlansStrings.subscribedMessage: get String
PlansStrings.restoredTitle: get String
PlansStrings.restoredMessage: get String
PlansStrings.done: get String
PlansStrings.errorTitle: get String
PlansStrings.errorMessage: get String
PlansStrings.offlineTitle: get String
PlansStrings.offlineMessage: get String
PlansStrings.retry: get StringStrings
Every label in English, French and Arabic. The screen follows the closest Localizations; the same table is in the manifest, ready for your ARB files.
| Key | en | fr | ar |
|---|---|---|---|
plansTitle | Choose a plan | Choisir une offre | اختر خطة |
plansClose | Close | Fermer | إغلاق |
plansLoading | Loading plans | Chargement des offres | جارٍ تحميل الخطط |
plansPeriod | Billing period | Période de facturation | فترة الفوترة |
plansMonthly | Monthly | Mensuel | شهري |
plansYearly | Yearly | Annuel | سنوي |
plansWithSavings | {period} {savings} | {period} {savings} | {period} {savings} |
plansPerMonth | per month | par mois | شهريًا |
plansPerYear | per year | par an | سنويًا |
plansPlans | Plans | Offres | الخطط |
plansRecommended | Recommended | Recommandée | موصى بها |
plansCurrent | Current plan | Offre actuelle | خطتك الحالية |
plansSubscribe | Subscribe | S’abonner | اشترك |
plansStartTrial | Start free trial | Commencer l’essai gratuit | ابدأ الفترة التجريبية المجانية |
plansCurrentAction | Your current plan | Votre offre actuelle | خطتك الحالية |
plansBilling | {price} {period}, renews automatically | {price} {period}, renouvelé automatiquement | {price} {period}، يتجدد تلقائيًا |
plansRenewal | The subscription renews automatically at the same price at the end of each period unless you cancel it before then. Cancel anytime in your App Store or Google Play account settings. | L’abonnement se renouvelle automatiquement au même prix à la fin de chaque période, sauf si vous le résiliez avant. Résiliez à tout moment dans les réglages de votre compte App Store ou Google Play. | يتجدد الاشتراك تلقائيًا بالسعر نفسه في نهاية كل فترة ما لم تُلغِه قبل ذلك. يمكنك الإلغاء في أي وقت من إعدادات حسابك في App Store أو Google Play. |
plansTerms | Terms of use | Conditions d’utilisation | شروط الاستخدام |
plansPrivacy | Privacy policy | Politique de confidentialité | سياسة الخصوصية |
plansRestore | Restore | Rétablir | استعادة |
plansRestorePurchases | Restore purchases | Rétablir les achats | استعادة المشتريات |
plansManage | Manage subscription | Gérer l’abonnement | إدارة الاشتراك |
plansPurchaseDeclined | The purchase was declined. You weren’t charged. | L’achat a été refusé. Rien n’a été débité. | تم رفض عملية الشراء. لم يُخصم أي مبلغ. |
plansPurchaseUnconfirmed | We couldn’t confirm your purchase. If you were charged, restore your purchases. | L’achat n’a pas pu être confirmé. Si vous avez été débité, rétablissez vos achats. | تعذّر تأكيد عملية الشراء. إذا تم خصم المبلغ، استعد مشترياتك. |
plansPurchaseOffline | The connection was lost during the purchase. If you were charged, restore your purchases once online. | La connexion s’est perdue pendant l’achat. Si vous avez été débité, rétablissez vos achats une fois connecté. | انقطع الاتصال أثناء الشراء. إذا تم خصم المبلغ، استعد مشترياتك عند الاتصال. |
plansAwaitingApproval | Waiting for approval | En attente d’approbation | بانتظار الموافقة |
plansPending | Your purchase is waiting for approval. Your plan starts once it’s approved. | Votre achat attend une approbation. Votre offre démarrera une fois l’achat approuvé. | عملية الشراء بانتظار الموافقة. ستبدأ خطتك بعد الموافقة عليها. |
plansNothingToRestore | No purchases to restore for this account. | Aucun achat à rétablir pour ce compte. | لا توجد مشتريات لاستعادتها لهذا الحساب. |
plansRestoreFailed | Purchases couldn’t be restored. Try again. | Impossible de rétablir les achats. Réessayez. | تعذّرت استعادة المشتريات. أعد المحاولة. |
plansRestoreOffline | You are offline. Connect, then restore your purchases. | Vous êtes hors ligne. Connectez-vous, puis rétablissez vos achats. | أنت غير متصل. اتصل ثم استعد مشترياتك. |
plansSubscribedTitle | Welcome to {plan} | Bienvenue dans {plan} | مرحبًا بك في {plan} |
plansSubscribedMessage | Your subscription is active on all your devices. | Votre abonnement est actif sur tous vos appareils. | اشتراكك نشط على جميع أجهزتك. |
plansRestoredTitle | Purchases restored | Achats rétablis | تمت استعادة المشتريات |
plansRestoredMessage | Your subscription is active again on this device. | Votre abonnement est de nouveau actif sur cet appareil. | اشتراكك نشط مجددًا على هذا الجهاز. |
plansDone | Continue | Continuer | متابعة |
plansErrorTitle | Plans could not load | Impossible de charger les offres | تعذّر تحميل الخطط |
plansErrorMessage | Something went wrong on our side. | Un problème est survenu de notre côté. | حدثت مشكلة من جهتنا. |
plansOfflineTitle | You are offline | Vous êtes hors ligne | أنت غير متصل |
plansOfflineMessage | Check your connection, then try again. | Vérifiez votre connexion, puis réessayez. | تحقق من اتصالك ثم أعد المحاولة. |
plansRetry | Try again | Réessayer | أعد المحاولة |
Reference renders
The 16 images published with version 0.2.5, rendered by the shared harness on macOS with Flutter 3.47.7. Each shows every state side by side, at the size of an iPhone (390 × 844) or of an Android phone (412 × 915). fliin verify plans compares your copy to them.
















Use it to
- offer a monthly or yearly subscription with several tiers on a paywall
- offer in-app purchase of a subscription on the App Store and Google Play
- show the discount of the yearly option over paying each month
- offer a free trial before the first charge
- let people restore purchases made on another device
- move up to a higher tier from the current one
- Not for
- a single plan to pick in a list: use radio buttons (radio)
- a cart of items to pay for: use the checkout screen (checkout)
- States