Fliin
On this page

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.

16 of 16 variants pass
Plans and subscription, ready state
Platform
Brightness
Text scale
Direction

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 plans

Also 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.

Plans and subscription, ready state
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.

Plans and subscription, loading state
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.

Plans and subscription, purchasing state
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.

Plans and subscription, purchased state
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.

Plans and subscription, failed state
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.

Plans and subscription, error state
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.

Plans and subscription, offline state
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.

Plans and subscription, header state
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 String

Strings

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.

Keyenfrar
plansTitleChoose a planChoisir une offreاختر خطة
plansCloseCloseFermerإغلاق
plansLoadingLoading plansChargement des offresجارٍ تحميل الخطط
plansPeriodBilling periodPériode de facturationفترة الفوترة
plansMonthlyMonthlyMensuelشهري
plansYearlyYearlyAnnuelسنوي
plansWithSavings{period} {savings}{period} {savings}{period} {savings}
plansPerMonthper monthpar moisشهريًا
plansPerYearper yearpar anسنويًا
plansPlansPlansOffresالخطط
plansRecommendedRecommendedRecommandéeموصى بها
plansCurrentCurrent planOffre actuelleخطتك الحالية
plansSubscribeSubscribeS’abonnerاشترك
plansStartTrialStart free trialCommencer l’essai gratuitابدأ الفترة التجريبية المجانية
plansCurrentActionYour current planVotre offre actuelleخطتك الحالية
plansBilling{price} {period}, renews automatically{price} {period}, renouvelé automatiquement{price} {period}، يتجدد تلقائيًا
plansRenewalThe 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.
plansTermsTerms of useConditions d’utilisationشروط الاستخدام
plansPrivacyPrivacy policyPolitique de confidentialitéسياسة الخصوصية
plansRestoreRestoreRétablirاستعادة
plansRestorePurchasesRestore purchasesRétablir les achatsاستعادة المشتريات
plansManageManage subscriptionGérer l’abonnementإدارة الاشتراك
plansPurchaseDeclinedThe purchase was declined. You weren’t charged.L’achat a été refusé. Rien n’a été débité.تم رفض عملية الشراء. لم يُخصم أي مبلغ.
plansPurchaseUnconfirmedWe 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.تعذّر تأكيد عملية الشراء. إذا تم خصم المبلغ، استعد مشترياتك.
plansPurchaseOfflineThe 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é.انقطع الاتصال أثناء الشراء. إذا تم خصم المبلغ، استعد مشترياتك عند الاتصال.
plansAwaitingApprovalWaiting for approvalEn attente d’approbationبانتظار الموافقة
plansPendingYour 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é.عملية الشراء بانتظار الموافقة. ستبدأ خطتك بعد الموافقة عليها.
plansNothingToRestoreNo purchases to restore for this account.Aucun achat à rétablir pour ce compte.لا توجد مشتريات لاستعادتها لهذا الحساب.
plansRestoreFailedPurchases couldn’t be restored. Try again.Impossible de rétablir les achats. Réessayez.تعذّرت استعادة المشتريات. أعد المحاولة.
plansRestoreOfflineYou are offline. Connect, then restore your purchases.Vous êtes hors ligne. Connectez-vous, puis rétablissez vos achats.أنت غير متصل. اتصل ثم استعد مشترياتك.
plansSubscribedTitleWelcome to {plan}Bienvenue dans {plan}مرحبًا بك في {plan}
plansSubscribedMessageYour subscription is active on all your devices.Votre abonnement est actif sur tous vos appareils.اشتراكك نشط على جميع أجهزتك.
plansRestoredTitlePurchases restoredAchats rétablisتمت استعادة المشتريات
plansRestoredMessageYour subscription is active again on this device.Votre abonnement est de nouveau actif sur cet appareil.اشتراكك نشط مجددًا على هذا الجهاز.
plansDoneContinueContinuerمتابعة
plansErrorTitlePlans could not loadImpossible de charger les offresتعذّر تحميل الخطط
plansErrorMessageSomething went wrong on our side.Un problème est survenu de notre côté.حدثت مشكلة من جهتنا.
plansOfflineTitleYou are offlineVous êtes hors ligneأنت غير متصل
plansOfflineMessageCheck your connection, then try again.Vérifiez votre connexion, puis réessayez.تحقق من اتصالك ثم أعد المحاولة.
plansRetryTry againRé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.

iOS · Light
Plans and subscription, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Plans and subscription, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Plans and subscription, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Plans and subscription, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Plans and subscription, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Plans and subscription, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Plans and subscription, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Plans and subscription, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Plans and subscription, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Plans and subscription, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Plans and subscription, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Plans and subscription, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Plans and subscription, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Plans and subscription, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Plans and subscription, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Plans and subscription, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

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
  • Ready
  • Loading
  • Purchasing
  • Purchased
  • Failed
  • Error
  • Offline

Arrows to move, Enter to open, Escape to close.