Ship with confidence. Lock the app until you get paid.
Set an expiry date. Ship your app. Everything else is handled.
Getting Started · Remote Expiry · API Reference · Customization · pub.dev →
Overview
🔒 Expiry Deadlline App |
🖼 Branding Watermark |
ExpiryApp(
expiryDate: DateTime(2026, 12, 31),
child: const MyApp(),
)
That's it. One widget wraps everything.
✦ Features
| 🔒 Payment deadline gate | App locks on your terms — no payment, no access |
| 🎨 Beautiful default UI | Animated lock screen with gradient, shown out of the box |
| 🛠 Fully replaceable | Swap in your own expired widget with a single parameter |
| 📅 ExpiryService utility | Query remaining days or duration programmatically |
| 🌐 Remote expiry date | Fetch the expiry date from your server — update without republishing |
| 🖼 Unpaid branding | Stamp every screen with your watermark until the client pays |
| 🪶 Zero extra dependencies | Pure Flutter — nothing added to your dependency tree |
📦 Getting Started
1. Install
# pubspec.yaml
dependencies:
flutter_app_expiry: ^latest-version
flutter pub get
2. Wrap your app
import 'package:flutter/material.dart';
import 'package:flutter_app_expiry/flutter_app_expiry.dart';
void main() {
runApp(
ExpiryApp(
expiryDate: DateTime(2026, 12, 31), // 🔑 set your date
child: const MyApp(),
),
);
}
Done. The app runs normally until expiryDate — then the lock screen takes over automatically.
🎨 Customization
Option A — Use your own expired screen
ExpiryApp(
expiryDate: DateTime(2026, 12, 31),
expiredWidget: const MyCustomExpiredScreen(), // full control
child: const MyApp(),
)
Option B — Customize the built-in screen
ExpiryApp(
expiryDate: DateTime(2026, 12, 31),
expiredTitle: 'Trial Ended',
expiredMessage: 'Please purchase a license to continue.',
contactInfo: 'support@yourapp.com',
child: const MyApp(),
)
Option C — Embed DefaultExpiredScreen directly
DefaultExpiredScreen(
expiryDate: DateTime(2026, 12, 31),
title: 'License Expired',
message: 'Renew to keep going.',
contactInfo: 'hello@example.com',
)
🖼 Watermark Overlay
Need to brand a demo build or remind unpaid clients they're on a trial? The watermark overlay floats above your entire app UI — persistent, unobtrusive, and impossible to miss.
Common use cases:
- Demo apps — make it clear the build isn't production-ready
- Unpaid clients — reinforce that a license is still pending
- Internal builds — distinguish staging from release
Text watermark (default)
ExpiryApp(
expiryDate: DateTime(2026, 12, 31),
watermark: WatermarkConfig(
text: 'DEMO',
color: Colors.red,
opacity: 0.15,
angle: -0.4,
repeat: true,
tileSpacing: 140.0,
),
child: const MyApp(),
)
Image watermark
Pass any asset image path via imagePath. Make sure the asset is declared in your pubspec.yaml.
ExpiryApp(
expiryDate: DateTime(2026, 12, 31),
watermark: WatermarkConfig(
imagePath: 'assets/logo.png', // your asset path
imageSize: Size(80, 80), // tile size (optional, default 80×80)
opacity: 0.15,
angle: -0.4,
repeat: true,
tileSpacing: 140.0,
),
child: const MyApp(),
)
Note: When
imagePathis set,text,textStyle, andcolorare ignored — image mode takes priority.
Single centered image (no tiling):
WatermarkConfig(
imagePath: 'assets/logo.png',
repeat: false, // renders once, centered
)
The watermark renders above your app's UI at all times and ignores all touch events so it never blocks user interaction.
🧮 ExpiryService
Need to react to expiry in your own logic? Use ExpiryService for programmatic checks:
final service = ExpiryService();
final expiry = DateTime(2026, 12, 31);
// Check expiry state
final expired = service.isExpired(expiry); // bool
// How much time is left?
final days = service.remainingDays(expiry); // int (e.g. 42)
final duration = service.remainingDuration(expiry); // Duration
Great for showing in-app banners like "Your trial expires in 7 days".
🌐 Remote Expiry Date
Need to change the expiry date without republishing your app? Serve it from a JSON endpoint and RemoteExpiryService handles the rest — with caching, retries, and full web support built in.
Quickest way — one line
final date = await RemoteExpiryService.fetch('https://example.com/expiry.json');
Inside ExpiryApp (recommended)
void main() {
runApp(
ExpiryApp.remote(
remoteUrl: 'https://example.com/expiry.json',
fallbackExpiryDate: DateTime(2026, 12, 31), // used if server is unreachable
child: const MyApp(),
),
);
}
While the date is being fetched a loading spinner is shown. If the fetch fails, fallbackExpiryDate is used automatically.
Full configuration
final service = RemoteExpiryService(
url: 'https://example.com/expiry.json',
jsonKey: 'expiryDate', // JSON key to read (default: 'expiryDate')
cacheDuration: Duration(hours: 1), // cache in memory — avoids hitting server every launch
maxRetries: 2, // retry up to 2 extra times on failure
retryDelay: Duration(seconds: 1),
headers: {'Authorization': 'Bearer my-token'}, // optional auth headers
onError: (e) => debugPrint('Fetch error: $e'), // optional error callback
);
final date = await service.fetchExpiryDate();
Expected JSON format
Your endpoint must return a JSON object with an ISO 8601 date string:
{ "expiryDate": "2026-12-31T00:00:00Z" }
The key name is configurable via jsonKey. Dates are always normalised to UTC for consistency across all timezones.
Cache management
service.clearCache(); // force refresh for this URL only
RemoteExpiryService.clearAllCache(); // wipe cache for all URLs
Tip: You can host the JSON for free on GitHub Gist, Pastebin, or any static file host — no backend needed.
📚 API Reference
ExpiryApp
| Parameter | Type | Required | Description |
|---|---|---|---|
expiryDate |
DateTime |
✅ | The date the app will be blocked |
child |
Widget |
✅ | Your root app widget |
expiredWidget |
Widget? |
— | Fully custom expired screen |
expiredTitle |
String? |
— | Title on the default screen |
expiredMessage |
String? |
— | Message on the default screen |
contactInfo |
String? |
— | Contact text on the default screen |
watermark |
WatermarkConfig? |
— | Watermark overlay for demo/trial branding |
expiredWidgettakes precedence — if provided,expiredTitle,expiredMessage, andcontactInfoare ignored.
WatermarkConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
text |
String? |
'DEMO' |
Text label shown as watermark. Ignored when imagePath is set. |
textStyle |
TextStyle? |
null |
Custom style for text. Falls back to color + 24sp bold. |
imagePath |
String? |
null |
Asset path to an image watermark (e.g. 'assets/logo.png'). Takes priority over text. |
imageSize |
Size |
Size(80, 80) |
Width and height of each image tile. |
opacity |
double |
0.15 |
Overall opacity of the watermark (0.0–1.0). |
angle |
double |
-0.4 |
Rotation in radians. Negative = diagonal tilt. |
repeat |
bool |
true |
Tile the watermark across the whole screen. |
tileSpacing |
double |
140.0 |
Spacing between tiles when repeat is true. |
color |
Color |
Colors.grey |
Text color when no textStyle is provided. Text mode only. |
showOnExpiredScreen |
bool |
false |
Whether to show the watermark on the expired screen too. |
ExpiryService
| Method | Returns | Description |
|---|---|---|
isExpired(DateTime) |
bool |
true if the date has passed |
remainingDays(DateTime) |
int |
Full calendar days remaining |
remainingDuration(DateTime) |
Duration |
Precise duration until expiry |
RemoteExpiryService
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
String |
required | HTTPS endpoint returning the expiry JSON |
jsonKey |
String |
'expiryDate' |
JSON key holding the ISO 8601 date string |
timeout |
Duration |
10s |
Max wait time per request |
cacheDuration |
Duration |
1h |
How long to cache the result in memory |
maxRetries |
int |
2 |
Extra retry attempts on failure |
retryDelay |
Duration |
1s |
Delay between retries |
headers |
Map<String,String>? |
null |
Optional HTTP headers (e.g. auth tokens) |
onError |
Function(Object)? |
null |
Called after all retries fail |
| Method / Static | Returns | Description |
|---|---|---|
RemoteExpiryService.fetch(url) |
Future<DateTime?> |
One-shot fetch — no instance needed |
fetchExpiryDate() |
Future<DateTime?> |
Fetch with caching and retries |
clearCache() |
void |
Clear cache for this URL |
RemoteExpiryService.clearAllCache() |
void |
Clear cache for all URLs |
📂 Project Structure
flutter_app_expiry/
├── lib/
│ ├── flutter_app_expiry.dart # Public barrel export
│ └── src/
│ ├── expiry_service.dart # Core expiry logic
│ ├── expiry_screen.dart # Default animated UI
│ ├── expiry_widget.dart # ExpiryApp wrapper widget
│ ├── remote_expiry_service.dart # RemoteExpiryService (fetch from server)
│ ├── remote_expiry_fetcher_io.dart # HTTP impl for Android/iOS/Desktop
│ ├── remote_expiry_fetcher_web.dart# HTTP impl for Flutter Web
│ ├── remote_expiry_fetcher_stub.dart # Fallback stub
│ ├── watermark_config.dart # WatermarkConfig model
│ └── watermark_overlay.dart # Watermark overlay widget
├── example/
│ └── lib/main.dart
├── pubspec.yaml
├── CHANGELOG.md
├── README.md
└── LICENSE
🤝 Contributing
Issues and pull requests are welcome! If you find a bug or have a feature idea, please open an issue.
- Fork the repo
- Create your feature branch:
git checkout -b feat/amazing-thing - Commit your changes:
git commit -m 'feat: add amazing thing' - Push and open a PR
📄 License
MIT © see LICENSE for details.
Made with ♥ for the Flutter community
Libraries
- flutter_app_expiry
- A Flutter package to enforce app expiration (trial period) based on a specific date.