From 58863b89eefec0b8c120934a250f3b322fbc8421 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Thu, 25 Jan 2024 16:56:31 +1100 Subject: [PATCH 1/5] load PFIs from known source and cache as shared preference --- frontend/ios/Podfile.lock | 7 ++ frontend/lib/features/pfis/pfi_providers.dart | 72 ++++++++++++++----- frontend/lib/features/pfis/pfis_page.dart | 44 +++++++----- frontend/pubspec.lock | 66 ++++++++++++++++- frontend/pubspec.yaml | 1 + 5 files changed, 152 insertions(+), 38 deletions(-) diff --git a/frontend/ios/Podfile.lock b/frontend/ios/Podfile.lock index 8069c477..698b6081 100644 --- a/frontend/ios/Podfile.lock +++ b/frontend/ios/Podfile.lock @@ -5,6 +5,9 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - web5_flutter (0.0.1): - Flutter - webview_flutter_wkwebview (0.0.1): @@ -14,6 +17,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - web5_flutter (from `.symlinks/plugins/web5_flutter/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) @@ -24,6 +28,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" web5_flutter: :path: ".symlinks/plugins/web5_flutter/ios" webview_flutter_wkwebview: @@ -33,6 +39,7 @@ SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 web5_flutter: 9d0f3466d7bef47a35cf92aca8dbb30465c63d2e webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a diff --git a/frontend/lib/features/pfis/pfi_providers.dart b/frontend/lib/features/pfis/pfi_providers.dart index d69ec83b..924a59cd 100644 --- a/frontend/lib/features/pfis/pfi_providers.dart +++ b/frontend/lib/features/pfis/pfi_providers.dart @@ -1,22 +1,56 @@ +import 'dart:convert'; import 'package:flutter_starter/features/pfis/pfi.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; -final pfisProvider = Provider>( - (ref) => [ - Pfi( - id: 'prototype', - name: 'Prototype', - didUri: 'did:dht:74hg1efatndi8enx3e4z6c4u8ieh1xfkyay4ntg4dg1w6risu35y', - ), - Pfi( - id: 'africa', - name: 'Africa', - didUri: 'coming soon...', - ), - Pfi( - id: 'mexico', - name: 'Mexico', - didUri: 'coming soon...', - ), - ], -); +final pfisProvider = FutureProvider>((ref) async { + const url = 'https://raw.githubusercontent.com/TBD54566975/pfi-providers-data/main/pfis.json'; + const cacheKey = 'pfis_cache'; + + try { + // Attempt to fetch data from the URL + final response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + // Parse the JSON data directly into a list of Pfi objects + List pfis = (json.decode(response.body) as List).map((data) { + return Pfi( + id: data['id'] as String, + name: data['name'] as String, + didUri: data['didUri'] as String, + ); + }).toList(); + + // Save the data to cache + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(cacheKey, response.body); + + return pfis; + } else { + // If server returns an unsuccessful response, try loading from cache + return await _loadFromCache(cacheKey); + } + } catch (e) { + // In case of an error, try loading from cache + return await _loadFromCache(cacheKey); + } +}); + +Future> _loadFromCache(String cacheKey) async { + final prefs = await SharedPreferences.getInstance(); + String? cachedData = prefs.getString(cacheKey); + + if (cachedData != null) { + // If cached data is available, parse and return it + return (json.decode(cachedData) as List).map((data) { + return Pfi( + id: data['id'] as String, + name: data['name'] as String, + didUri: data['didUri'] as String, + ); + }).toList(); + } else { + // If there is no cached data, return an empty list or handle appropriately + return []; + } +} diff --git a/frontend/lib/features/pfis/pfis_page.dart b/frontend/lib/features/pfis/pfis_page.dart index 1f5c5467..e1f6e186 100644 --- a/frontend/lib/features/pfis/pfis_page.dart +++ b/frontend/lib/features/pfis/pfis_page.dart @@ -3,32 +3,40 @@ import 'package:flutter_starter/features/pfis/pfi_providers.dart'; import 'package:flutter_starter/features/pfis/pfi_verification_page.dart'; import 'package:flutter_starter/l10n/app_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_starter/features/pfis/pfi.dart'; class PfisPage extends HookConsumerWidget { const PfisPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final pfis = ref.watch(pfisProvider); + // Watch the provider and get the AsyncValue object + AsyncValue> pfisAsyncValue = ref.watch(pfisProvider); + return Scaffold( appBar: AppBar(title: Text(Loc.of(context).selectYourRegion)), - body: ListView( - children: [ - ...pfis.map( - (pfi) => ListTile( - title: Text(pfi.name), - subtitle: Text(pfi.didUri), - trailing: const Icon(Icons.chevron_right), - onTap: () async { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => PfiVerificationPage(pfi: pfi), - ), - ); - }, - ), - ) - ], + body: pfisAsyncValue.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + data: (pfis) { + // Build the list when data is available + return ListView( + children: pfis.map( + (pfi) => ListTile( + title: Text(pfi.name), + subtitle: Text(pfi.didUri), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => PfiVerificationPage(pfi: pfi), + ), + ); + }, + ), + ).toList(), + ); + }, ), ); } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 3153db14..be63ad1d 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -349,6 +357,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.9" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" sky_engine: dependency: transitive description: flutter @@ -502,4 +566,4 @@ packages: version: "1.0.4" sdks: dart: ">=3.2.3 <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.16.0" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index a6f2a1b6..6a33dfbd 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: http: ^1.1.2 intl: ^0.18.1 webview_flutter: ^4.4.2 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: From 14c8cb4beb8d6a087ed0c461644ce2ed352e815d Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Fri, 26 Jan 2024 14:10:02 +1100 Subject: [PATCH 2/5] use ref directly for smarter reload --- frontend/lib/features/pfis/pfis_page.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/lib/features/pfis/pfis_page.dart b/frontend/lib/features/pfis/pfis_page.dart index e1f6e186..19333ccb 100644 --- a/frontend/lib/features/pfis/pfis_page.dart +++ b/frontend/lib/features/pfis/pfis_page.dart @@ -3,19 +3,17 @@ import 'package:flutter_starter/features/pfis/pfi_providers.dart'; import 'package:flutter_starter/features/pfis/pfi_verification_page.dart'; import 'package:flutter_starter/l10n/app_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:flutter_starter/features/pfis/pfi.dart'; class PfisPage extends HookConsumerWidget { const PfisPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - // Watch the provider and get the AsyncValue object - AsyncValue> pfisAsyncValue = ref.watch(pfisProvider); + // Watch the provider and get the AsyncValue object return Scaffold( appBar: AppBar(title: Text(Loc.of(context).selectYourRegion)), - body: pfisAsyncValue.when( + body: ref.watch(pfisProvider).when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (pfis) { From 19f22cd772fd9d2e520bb4b8d560a2d4f216fe5d Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Fri, 26 Jan 2024 17:04:21 +1100 Subject: [PATCH 3/5] load from cache first --- frontend/lib/features/pfis/pfi_providers.dart | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/frontend/lib/features/pfis/pfi_providers.dart b/frontend/lib/features/pfis/pfi_providers.dart index 924a59cd..78791fe8 100644 --- a/frontend/lib/features/pfis/pfi_providers.dart +++ b/frontend/lib/features/pfis/pfi_providers.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter_starter/features/pfis/pfi.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart' as http; @@ -8,31 +9,16 @@ final pfisProvider = FutureProvider>((ref) async { const url = 'https://raw.githubusercontent.com/TBD54566975/pfi-providers-data/main/pfis.json'; const cacheKey = 'pfis_cache'; - try { - // Attempt to fetch data from the URL - final response = await http.get(Uri.parse(url)); - if (response.statusCode == 200) { - // Parse the JSON data directly into a list of Pfi objects - List pfis = (json.decode(response.body) as List).map((data) { - return Pfi( - id: data['id'] as String, - name: data['name'] as String, - didUri: data['didUri'] as String, - ); - }).toList(); - - // Save the data to cache - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(cacheKey, response.body); - - return pfis; - } else { - // If server returns an unsuccessful response, try loading from cache - return await _loadFromCache(cacheKey); - } - } catch (e) { - // In case of an error, try loading from cache - return await _loadFromCache(cacheKey); + // First, try loading from cache + List pfis = await _loadFromCache(cacheKey); + if (pfis.isNotEmpty) { + // If cache has data, return it first + // Then, asynchronously refresh the cache + _refreshCache(url, cacheKey); + return pfis; + } else { + // If cache is empty, fetch from the URL + return await _fetchFromURL(url, cacheKey); } }); @@ -41,7 +27,6 @@ Future> _loadFromCache(String cacheKey) async { String? cachedData = prefs.getString(cacheKey); if (cachedData != null) { - // If cached data is available, parse and return it return (json.decode(cachedData) as List).map((data) { return Pfi( id: data['id'] as String, @@ -50,7 +35,41 @@ Future> _loadFromCache(String cacheKey) async { ); }).toList(); } else { - // If there is no cached data, return an empty list or handle appropriately return []; } } + +Future> _fetchFromURL(String url, String cacheKey) async { + try { + final response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(cacheKey, response.body); + + return (json.decode(response.body) as List).map((data) { + return Pfi( + id: data['id'] as String, + name: data['name'] as String, + didUri: data['didUri'] as String, + ); + }).toList(); + } + } catch (e) { + // Handle the error or return an empty list + debugPrint(e.toString()); + } + return []; +} + +Future _refreshCache(String url, String cacheKey) async { + try { + final response = await http.get(Uri.parse(url)); + if (response.statusCode == 200) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(cacheKey, response.body); + } + } catch (e) { + // Handle the error silently as this is a background refresh + debugPrint(e.toString()); + } +} From ab657e67168ec7acd95f9a0bd38ee631aaecb09e Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Fri, 26 Jan 2024 18:03:24 +1100 Subject: [PATCH 4/5] allow setting local dev PFI via comment line --- frontend/lib/features/pfis/pfi_providers.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/lib/features/pfis/pfi_providers.dart b/frontend/lib/features/pfis/pfi_providers.dart index 78791fe8..513e4eea 100644 --- a/frontend/lib/features/pfis/pfi_providers.dart +++ b/frontend/lib/features/pfis/pfi_providers.dart @@ -7,7 +7,20 @@ import 'package:shared_preferences/shared_preferences.dart'; final pfisProvider = FutureProvider>((ref) async { const url = 'https://raw.githubusercontent.com/TBD54566975/pfi-providers-data/main/pfis.json'; - const cacheKey = 'pfis_cache'; + const cacheKey = 'pfi_cache'; + + + // fall back to a dev PFI if passed on command line like: flutter run --dart-define=DEV_PFI=your_did_string + const devPfi = String.fromEnvironment('DEV_PFI'); + if (devPfi != '' && devPfi != null) { + return [ + Pfi( + id: 'dev', + name: 'Dev PFI', + didUri: devPfi, + ), + ]; + } // First, try loading from cache List pfis = await _loadFromCache(cacheKey); From 8afcd37362d037b6db1497b8039405adbd246849 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Fri, 26 Jan 2024 20:20:13 +1100 Subject: [PATCH 5/5] readme note --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 142689c9..de3a0111 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The aim is that this app can work with any tbdex liquidity node, discoverying th * Install Hermit https://cashapp.github.io/hermit/ (on macos you can run `brew install hermit` and then `hermit shell-hooks`) * Ensure you have a mobile app simulator handy (XCode on macOS and runing the Simulator app will do for example) -* Run `flutter run` from this project to build and start the app in the simulator +* Run `flutter run` from this project to build and start the app in the simulator. Use `flutter run --dart-define=DEV_PFI=your_did_string` to run against a local tbdex liquidity node at dev time.