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
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user