// Install confirmation with provenance + trust context. The // usertest security persona flagged one-click installs with no // visible trust signal as the platform's biggest supply-chain // gap: every install path (store card, detail sheet, flow-list // quick fix) now routes through this dialog, which shows what // the hub actually knows before anything is downloaded — origin // store, version, license, maturity, required services — plus // an honest note on when signature verification happens. No // fake "verified" badge: per-entry signature status is not in // the store index yet, and the dialog says so instead of // pretending. import 'package:flutter/material.dart'; import '../data/hub.dart'; import '../l10n/app_localizations.dart'; import '../theme/tokens.dart'; class ChainInstallConfirmDialog extends StatelessWidget { final StoreItem item; const ChainInstallConfirmDialog({super.key, required this.item}); /// Confirm installing a known store entry. Returns true to /// proceed. static Future show(BuildContext context, StoreItem item) async { final r = await showDialog( context: context, builder: (_) => ChainInstallConfirmDialog(item: item), ); return r == true; } /// Confirm installing by bare capability name (the flow-list /// quick-fix path). Looks the entry up in the store index so /// the dialog shows the same provenance as the Store page; /// falls back to a minimal entry when the index has no match /// (the hub will reject unknown names on install anyway). static Future showForCapability( BuildContext context, String capability, ) async { StoreItem? match; try { final hits = await HubService.instance.searchStore( query: capability, limit: 10, ); for (final h in hits) { if (h.name == capability) { match = h; break; } } } catch (_) { // Unreachable store index — fall through to the minimal // entry; the dialog still explains the trust model. } if (!context.mounted) return false; return show( context, match ?? StoreItem( name: capability, taglineEn: '', taglineDe: '', descriptionEn: '', descriptionDe: '', category: '', tags: const [], requiresCapabilities: const [], requiresServices: const [], license: '', repository: '', bestVersion: '', status: '', installed: false, featured: false, iconUrl: '', screenshotUrls: const [], docsUrl: '', kind: '', provider: '', sourceKind: '', source: '', canonicalCategory: '', canonicalCategoryLabel: '', ), ); } String _statusLabel(AppLocalizations l) => switch (item.status) { 'published' => l.storeStatusPublished, 'alpha' => l.storeStatusAlpha, 'planned' => l.storeStatusPlanned, _ => item.status, }; @override Widget build(BuildContext context) { final theme = Theme.of(context); final l = AppLocalizations.of(context)!; final rows = <(String, String)>[ if (item.bestVersion.isNotEmpty) (l.installConfirmVersion, 'v${item.bestVersion}'), ( l.installConfirmSource, item.source.isEmpty || item.source == 'bundled' ? l.installConfirmSourceBundled : item.source, ), if (item.license.isNotEmpty) (l.installConfirmLicense, item.license), if (item.status.isNotEmpty) (l.installConfirmStatus, _statusLabel(l)), if (item.requiresServices.isNotEmpty) (l.installConfirmNeedsServices, item.requiresServices.join(', ')), if (item.requiresCapabilities.isNotEmpty) ( l.installConfirmNeedsCapabilities, item.requiresCapabilities.join(', ') ), ]; return AlertDialog( title: Text(l.installConfirmTitle(item.name)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ChainRadius.md), ), content: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 460), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final (label, value) in rows) Padding( padding: const EdgeInsets.only(bottom: ChainSpace.xs), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 140, child: Text( label, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), Expanded( child: Text(value, style: theme.textTheme.bodySmall), ), ], ), ), const SizedBox(height: ChainSpace.sm), Container( padding: const EdgeInsets.all(ChainSpace.md), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(ChainRadius.sm), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.shield_outlined, size: 14, color: theme.colorScheme.primary, ), const SizedBox(width: 6), Text( l.installConfirmTrustTitle, style: theme.textTheme.labelMedium, ), ], ), const SizedBox(height: ChainSpace.xs), Text( l.installConfirmTrustBody, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, height: 1.4, ), ), const SizedBox(height: ChainSpace.xs), Text( l.installConfirmSignatureNote, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, height: 1.4, ), ), ], ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text(l.buttonCancel), ), FilledButton( onPressed: () => Navigator.pop(context, true), child: Text(l.buttonInstall), ), ], ); } }