🚀 Join the waitlist now! waitlist.floot.dev
LogoFlootdocs
Payments

App Paywall

Learn how to set up, configure, and personalize the built-in paywall for your Floot app.

After setting up Stripe or RevenueCat, any plans you create will automatically appear on the paywall page. Below are instructions on how to further customize the paywall and add extra features.

Lifetime Subscriptions

Create Lifetime Price

By default, the paywall displays only recurring plans. If you want to offer a lifetime deal, create a one-time payment offer in your payment provider (Stripe or RevenueCat) following the Floot guides. After creating the offer, it will appear in your Supabase prices table with the interval column set to NULL (the default for non-recurring prices). To display it on the paywall, update the interval value to lifetime by executing the following SQL command in your Supabase SQL editor:

UPDATE prices
SET interval = 'lifetime'
WHERE id = 'lifetime_price_id';

Handle server-side logic

After configuring lifetime prices, implement the server-side logic to process one-time payments for Stripe or RevenueCat.

Below is an example of creating a subscription that remains active for 100 years.

const subscriptionData: TablesInsert<"subscriptions"> = {
  id: uuidv4(),
  user_id: user.id,
  price_id: "lifetime_price_id",
  status: "active",
  current_period_start: new Date().toISOString(),
  // add 100 years to the current date
  current_period_end: new Date(
    new Date().setFullYear(new Date().getFullYear() + 100),
  ).toISOString(),
  cancel_at_period_end: false,
};

const { error } = await supabase.from("subscriptions").insert(subscriptionData);

if (error) {
  console.error(
    `DB: Error creating subscription ${subscriptionData.id}:`,
    error.message,
  );
  throw error;
}

return console.info(
  `🎉 DB: Lifetime subscription created successfully: ${subscriptionData.id}, for user with email: ${user.email}`,
);

Plans descriptions

By default, plan descriptions are taken from your payment provider. To customize these descriptions, for example, to support localization, provide the descriptions map in the PriceListView widget of your PaywallPage. Note: Keys are case sensitive.

Example with two plans named Basic and Premium:

lib/features/payments/presentation/views/paywall/paywall_page.dart
const PriceListView(
	descriptions: <String, String>{
		'Basic': 'Perfect for getting started.',
		'Premium': 'Best for growing startups and growth companies.',
	},
)

Plans features

To display the features included in each plan, pass a features map to the PriceListView widget in your PaywallPage. Note: The keys must exactly match your plan names (case sensitive).

Example with two plans named Basic and Premium:

lib/features/payments/presentation/views/paywall/paywall_page.dart
const PriceListView(
	features: <String, List<FeatureCard>>{
		'Basic': [
			FeatureCard(
				icon: LucideIcons.timer,
				description: 'Access to basic features',
			),
			FeatureCard(
				icon: LucideIcons.lock,
				description: 'Secure your account',
			),
		],
		'Premium': [
			FeatureCard(
				icon: LucideIcons.infinity,
				description: 'Unlimited access to all features',
			),
			FeatureCard(
				icon: LucideIcons.zap,
				description: 'Premium performance boost',
			),
		],
	},
)

Apps without paywall

If your app does not require a paywall (for example, if it is free to use), you can bypass the paywall screen entirely. To do this, update the postAuthPath parameter in the AuthRouter.redirect function within your router_config.dart file to point to your desired route.

In the example below, users are redirected to the dashboard immediately after authentication.

lib/core/presentation/router/router_config.dart
GoRouter routerConfig(AuthBloc bloc) {
  return GoRouter(
    ...
    // * Top level guard
    redirect: (context, state) => AuthRouter.redirect(
      context,
      state,
      bloc: bloc,
      postAuthPath: (user) => '/dashboard',
    ),
    ...
  );
}