diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index a650911..6369d37 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -43,7 +43,11 @@ class AppDetailScreen extends HookConsumerWidget { '#f': {platform}, }, limit: 1, - and: (a) => {a.latestRelease, a.latestRelease.value?.latestMetadata}, + and: (a) => { + a.latestRelease, + a.latestRelease.value?.latestMetadata, + a.latestRelease.value?.latestAsset, + }, // stream=true ensures cached/local results render immediately; remote // results will merge in as they arrive. source: const LocalAndRemoteSource(relays: 'AppCatalog', stream: false), diff --git a/lib/screens/app_stack_screen.dart b/lib/screens/app_stack_screen.dart index 2fff288..2768c36 100644 --- a/lib/screens/app_stack_screen.dart +++ b/lib/screens/app_stack_screen.dart @@ -86,6 +86,7 @@ class _AppStackContentWithApps extends HookConsumerWidget { and: (app) => { app.latestRelease, app.latestRelease.value?.latestMetadata, + app.latestRelease.value?.latestAsset, }, source: const LocalAndRemoteSource(relays: 'AppCatalog'), subscriptionPrefix: 'app-stack-apps-${stack!.identifier}', diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index e0cee53..d902362 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -164,6 +164,7 @@ class _SearchResultsSection extends HookConsumerWidget { and: (app) => { app.latestRelease, app.latestRelease.value?.latestMetadata, + app.latestRelease.value?.latestAsset, }, // Force the search to hit the default relay group (relay.zapstore.dev) // so a connection appears in Debug Info when searching. diff --git a/lib/screens/user_screen.dart b/lib/screens/user_screen.dart index 500a98f..c869a80 100644 --- a/lib/screens/user_screen.dart +++ b/lib/screens/user_screen.dart @@ -43,6 +43,7 @@ class UserScreen extends HookConsumerWidget { and: (app) => { app.latestRelease, app.latestRelease.value?.latestMetadata, + app.latestRelease.value?.latestAsset, }, source: const LocalAndRemoteSource(relays: 'AppCatalog', stream: false), subscriptionPrefix: 'user-apps', diff --git a/lib/services/updates_service.dart b/lib/services/updates_service.dart index 5b421e3..7e02a58 100644 --- a/lib/services/updates_service.dart +++ b/lib/services/updates_service.dart @@ -80,6 +80,7 @@ class CategorizedAppsNotifier extends Notifier { and: (app) => { app.latestRelease, app.latestRelease.value?.latestMetadata, + app.latestRelease.value?.latestAsset, }, source: const LocalAndRemoteSource(relays: 'AppCatalog', stream: true), andSource: const LocalAndRemoteSource( diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart index b8211cc..b3fce1e 100644 --- a/lib/utils/extensions.dart +++ b/lib/utils/extensions.dart @@ -38,8 +38,10 @@ extension AppExt on App { ref.read(packageManagerProvider.notifier).isInstalled(identifier); /// Latest file metadata associated to the latest release + /// Prefers SoftwareAsset (new format) over FileMetadata (old format) /// Note: assumes latest metadata has been loaded for the current platform FileMetadata? get latestFileMetadata => + latestRelease.value?.latestAsset.value ?? latestRelease.value?.latestMetadata.value; /// Whether there is an update available for the installed app @@ -79,3 +81,30 @@ extension AppExt on App { extension WidgetRefExt on WidgetRef { Ref get ref => read(Provider((ref) => ref)); } + +/// Extension to handle both old and new format certificate hashes +extension FileMetadataExt on FileMetadata { + /// Returns the APK certificate hash, checking new format first then old + /// New format (SoftwareAsset): apkCertificateHashes (Set) + /// Old format (FileMetadata): apkSignatureHash (String?) + String? get certificateHash { + // Try new format first (SoftwareAsset has apkCertificateHashes) + if (this is SoftwareAsset) { + final hashes = (this as SoftwareAsset).apkCertificateHashes; + if (hashes.isNotEmpty) return hashes.first; + } + // Fall back to old format + return apkSignatureHash; + } + + /// Returns all APK certificate hashes + Set get certificateHashes { + // Try new format first + if (this is SoftwareAsset) { + final hashes = (this as SoftwareAsset).apkCertificateHashes; + if (hashes.isNotEmpty) return hashes; + } + // Fall back to old format (single hash as set) + return apkSignatureHash != null ? {apkSignatureHash!} : {}; + } +} diff --git a/lib/widgets/app_detail_widgets.dart b/lib/widgets/app_detail_widgets.dart index 406de1e..5b39bfd 100644 --- a/lib/widgets/app_detail_widgets.dart +++ b/lib/widgets/app_detail_widgets.dart @@ -718,7 +718,7 @@ class DebugVersionsSection extends HookConsumerWidget { final releasesState = ref.watch( query( tags: app.event.addressableIdTagMap, - and: (release) => {release.latestMetadata}, + and: (release) => {release.latestMetadata, release.latestAsset}, source: LocalAndRemoteSource(relays: 'AppCatalog', stream: false), subscriptionPrefix: 'debug-releases', ), diff --git a/lib/widgets/app_info_table.dart b/lib/widgets/app_info_table.dart index c0b5a11..96a2397 100644 --- a/lib/widgets/app_info_table.dart +++ b/lib/widgets/app_info_table.dart @@ -82,8 +82,8 @@ class AppInfoTable extends HookConsumerWidget { ); } - if (fileMetadata?.apkSignatureHash != null) { - final full = fileMetadata!.apkSignatureHash!; + if (fileMetadata?.certificateHash != null) { + final full = fileMetadata!.certificateHash!; rows.add( _InfoRow( label: 'Certificate hash', diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index dd334db..d0d62c2 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -719,8 +719,7 @@ class InstallButton extends ConsumerWidget { final updateVersion = app.latestFileMetadata?.version ?? 'Unknown'; final currentVersion = installedPackage?.version ?? 'Unknown'; final currentCertHash = installedPackage?.signatureHash ?? 'Unknown'; - final updateCertHash = - app.latestFileMetadata?.apkSignatureHash ?? 'Unknown'; + final updateCertHash = app.latestFileMetadata?.certificateHash ?? 'Unknown'; // Get author profile final author = app.author.value; diff --git a/lib/widgets/latest_releases_container.dart b/lib/widgets/latest_releases_container.dart index b4067f2..15c85f3 100644 --- a/lib/widgets/latest_releases_container.dart +++ b/lib/widgets/latest_releases_container.dart @@ -60,7 +60,10 @@ class LatestReleasesContainer extends HookConsumerWidget { ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 6, + ), child: Text( 'LATEST RELEASES', style: context.textTheme.labelLarge?.copyWith( @@ -286,6 +289,7 @@ class LatestReleasesNotifier extends StateNotifier { and: (app) => { app.latestRelease, app.latestRelease.value?.latestMetadata, + app.latestRelease.value?.latestAsset, }, // NOTE: It must stream=true source: const LocalAndRemoteSource(relays: 'AppCatalog', stream: true), @@ -358,7 +362,7 @@ class LatestReleasesNotifier extends StateNotifier { source: const RemoteSource(stream: false), ); - // Load file metadata for the releases + // Load file metadata for the releases (old format) if (releases.isNotEmpty) { await ref.storage.query( Request( @@ -369,6 +373,16 @@ class LatestReleasesNotifier extends StateNotifier { ), source: const RemoteSource(stream: false), ); + // Load software assets for the releases (new format) + await ref.storage.query( + Request( + releases + .map((r) => r.latestAsset.req?.filters.firstOrNull) + .nonNulls + .toList(), + ), + source: const RemoteSource(stream: false), + ); } // Load release authors from different relay group (social)