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 createState() => _StorePageState(); } class _StorePageState extends State { List _products = []; String _status = ""; bool _busy = false; @override void initState() { super.initState(); _boot(); } Future _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 _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 _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"), ), ), ], ), ), ], ), ); } }