garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme, docs under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
143 lines
3.8 KiB
Dart
143 lines
3.8 KiB
Dart
import "package:flutter/material.dart";
|
|
import "package:garage_auth/garage_auth.dart";
|
|
import "package:garage_iap/garage_iap.dart";
|
|
|
|
// A tiny store screen: sign in, list products, buy one, show what you own. The
|
|
// real wiring (deep-link callback for completeSignIn etc.) is the host app's
|
|
// job — see garage_auth's README. This just shows the iap surface.
|
|
|
|
late final GarageAuth auth;
|
|
late final GarageIap iap;
|
|
|
|
void main() {
|
|
auth = GarageAuth(
|
|
issuer: "https://hub.imbenji.net/auth-api",
|
|
clientId: "example-app",
|
|
redirectUri: "exampleapp://auth/callback",
|
|
);
|
|
|
|
iap = GarageIap(
|
|
auth: auth,
|
|
appSlug: "example-app",
|
|
apiBaseUrl: "https://store.imbenji.net/api",
|
|
);
|
|
|
|
runApp(const ExampleApp());
|
|
}
|
|
|
|
class ExampleApp extends StatelessWidget {
|
|
const ExampleApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: "garage_iap example",
|
|
theme: ThemeData(useMaterial3: true),
|
|
home: const StorePage(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class StorePage extends StatefulWidget {
|
|
const StorePage({super.key});
|
|
|
|
@override
|
|
State<StorePage> createState() => _StorePageState();
|
|
}
|
|
|
|
class _StorePageState extends State<StorePage> {
|
|
List<GarageProduct> _products = [];
|
|
String _status = "";
|
|
bool _busy = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_boot();
|
|
}
|
|
|
|
Future<void> _boot() async {
|
|
await auth.restore();
|
|
|
|
// populate the offline gate cheaply before any network — cachedLicence reads
|
|
// straight from secure storage and verifies locally.
|
|
try {
|
|
await iap.cachedLicence();
|
|
} catch (e) {
|
|
// expired-offline etc. — fine, just means not entitled until we re-fetch.
|
|
print("cached licence not usable: $e");
|
|
}
|
|
|
|
if (auth.isSignedIn) {
|
|
await _load();
|
|
}
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
setState(() => _busy = true);
|
|
try {
|
|
_products = await iap.products();
|
|
await iap.entitlements();
|
|
await iap.licence(); // refresh + cache the offline licence
|
|
_status = "loaded";
|
|
} catch (e) {
|
|
_status = "load failed: $e";
|
|
}
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
|
|
Future<void> _buy(GarageProduct p) async {
|
|
setState(() => _busy = true);
|
|
try {
|
|
final outcome = await iap.purchase(p, mode: PurchaseMode.sheet);
|
|
_status = "purchase: ${outcome.name}";
|
|
} on IapError catch (e) {
|
|
_status = "purchase error: ${e.message}";
|
|
} catch (e) {
|
|
_status = "purchase failed: $e";
|
|
}
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text("Store")),
|
|
body: !auth.isSignedIn
|
|
? Center(
|
|
child: ElevatedButton(
|
|
onPressed: () => auth.signIn(),
|
|
child: const Text("Sign in with Garage"),
|
|
),
|
|
)
|
|
: Column(
|
|
children: [
|
|
if (_busy) const LinearProgressIndicator(),
|
|
Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Text(_status),
|
|
),
|
|
Expanded(
|
|
child: ListView(
|
|
children: [
|
|
for (final p in _products)
|
|
ListTile(
|
|
title: Text(p.name),
|
|
subtitle: Text(p.displayPrice),
|
|
trailing: iap.has(p.sku)
|
|
? const Chip(label: Text("Owned"))
|
|
: FilledButton(
|
|
onPressed: () => _buy(p),
|
|
child: const Text("Buy"),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|