Files
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
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
2026-09-23 18:49:21 +01:00

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"),
),
),
],
),
),
],
),
);
}
}