From 057f9b75f3328dfa264b2c4f7c694a5d4379c6ab Mon Sep 17 00:00:00 2001 From: Michael Bisgaard Olesen Date: Thu, 26 Oct 2023 21:50:54 +0200 Subject: [PATCH 1/4] Separate business logic from UI widgets Ensure that all business logic is performed in "pure" non-widget code. This improves code structure, testability etc. The widget `RefreshTermsAndConditionsScreen` is entirely removed as refresh logic doesn't belong in the view layer. All global (shared preferences) and network-specific (wallet-proxy) services are exposed as a "service repository" provided to the root of the widget tree along with separate watchable state components ("change notifiers") for selected network and T&C acceptance. The file state.dart is split into one file in `state/` for each of these state components. The currently valid T&C version is fetched when initializing the "home" screen. The time-based logic of reloading this version is removed as it isn't clear how it actually should be implemented. It probably should be only when we're opening/waking up the app. --- lib/main.dart | 64 ++++++++-- lib/screens/home/screen.dart | 119 ++++++++++++------ .../terms_and_conditions/refresh_screen.dart | 58 --------- lib/screens/terms_and_conditions/screen.dart | 36 +++--- lib/screens/terms_and_conditions/widget.dart | 4 +- lib/services/http.dart | 3 +- lib/services/shared_preferences/service.dart | 19 +++ lib/services/wallet_proxy/model.dart | 3 +- lib/services/wallet_proxy/service.dart | 7 +- lib/state.dart | 53 -------- lib/state/config.dart | 12 ++ lib/state/network.dart | 29 +++++ lib/state/services.dart | 26 ++++ lib/state/terms_and_conditions.dart | 73 +++++++++++ 14 files changed, 321 insertions(+), 185 deletions(-) delete mode 100644 lib/screens/terms_and_conditions/refresh_screen.dart create mode 100644 lib/services/shared_preferences/service.dart delete mode 100644 lib/state.dart create mode 100644 lib/state/config.dart create mode 100644 lib/state/network.dart create mode 100644 lib/state/services.dart create mode 100644 lib/state/terms_and_conditions.dart diff --git a/lib/main.dart b/lib/main.dart index 0bafb78..716f6e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,13 @@ -import 'package:concordium_wallet/state.dart'; +import 'package:concordium_wallet/screens/routes.dart'; +import 'package:concordium_wallet/services/http.dart'; +import 'package:concordium_wallet/services/shared_preferences/service.dart'; +import 'package:concordium_wallet/services/wallet_proxy/service.dart'; +import 'package:concordium_wallet/state/config.dart'; +import 'package:concordium_wallet/state/network.dart'; +import 'package:concordium_wallet/state/services.dart'; +import 'package:concordium_wallet/state/terms_and_conditions.dart'; import 'package:concordium_wallet/theme.dart'; import 'package:flutter/material.dart'; - -import 'package:concordium_wallet/screens/routes.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -10,6 +15,16 @@ void main() { runApp(const App()); } +final config = Config.ofNetworks([ + const Network( + name: NetworkName.testnet, + walletProxyConfig: WalletProxyConfig( + baseUrl: 'https://wallet-proxy.testnet.concordium.com', + ), + ), +]); +const httpService = HttpService(); + class App extends StatelessWidget { const App({super.key}); @@ -21,13 +36,44 @@ class App extends StatelessWidget { final prefs = snapshot.data; if (prefs == null) { // Loading preferences. - return const CircularProgressIndicator(); + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + // Setting text direction is required because we're outside 'MaterialApp' widget. + Text('Loading shared preferences...', textDirection: TextDirection.ltr), + ], + ); } - return ChangeNotifierProvider( - create: (context) => AppState(AppSharedPreferences(prefs)), - child: MaterialApp( - routes: appRoutes, - theme: concordiumTheme(), + // Initialize services and provide them to the nested components + // (including the state components created in the child provider). + return Provider( + create: (context) { + final testnet = config.availableNetworks[NetworkName.testnet]!; + final prefsSvc = SharedPreferencesService(prefs); + return ServiceRepository( + networkServices: {testnet: NetworkServices.forNetwork(testnet, httpService: httpService)}, + sharedPreferences: prefsSvc, + ); + }, + child: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => SelectedNetwork(config.availableNetworks[NetworkName.testnet]!), + ), + ChangeNotifierProvider( + create: (context) { + final prefs = context.read().sharedPreferences; + return TermsAndConditionAcceptance(prefs); + }, + ), + ], + child: MaterialApp( + routes: appRoutes, + theme: concordiumTheme(), + ), ), ); }, diff --git a/lib/screens/home/screen.dart b/lib/screens/home/screen.dart index 59d372e..87cc97e 100644 --- a/lib/screens/home/screen.dart +++ b/lib/screens/home/screen.dart @@ -1,54 +1,99 @@ -import 'package:concordium_wallet/screens/terms_and_conditions/refresh_screen.dart'; -import 'package:concordium_wallet/state.dart'; +import 'package:concordium_wallet/screens/terms_and_conditions/screen.dart'; +import 'package:concordium_wallet/services/wallet_proxy/service.dart'; +import 'package:concordium_wallet/state/network.dart'; +import 'package:concordium_wallet/state/services.dart'; +import 'package:concordium_wallet/state/terms_and_conditions.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class HomeScreen extends StatelessWidget { +class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override - Widget build(BuildContext context) { - final tacLastCheckedAt = context.select((AppState state) => state.termsAndConditionsLastVerifiedAt); - //print('tacLastCheckedAt: $tacLastCheckedAt'); + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + void initState() { + super.initState(); - // Temporary... - final state = context.watch(); - var lastAcceptedVersion = state.sharedPreferences.termsAndConditionsAcceptedVersion; - // print('lastAcceptedVersion: $lastAcceptedVersion'); + // Fetch currently valid T&C version unconditionally when initializing the widget. + final tacAcceptance = context.read(); + final network = context.read(); + final services = context.read().networkServices[network.selected]!; + _updateValidTac(services.walletProxy, tacAcceptance); + } - // Force recheck after 1 min. - // print('diff: ${DateTime.now().difference(tacLastCheckedAt).inMinutes}'); - if (DateTime.now().difference(tacLastCheckedAt).inMinutes > 1) { - return const RefreshTermsAndConditionsScreen(); - } + static Future _updateValidTac(WalletProxyService walletProxy, TermsAndConditionAcceptance tacAcceptance) async { + final tac = await walletProxy.getTermsAndConditions(); + tacAcceptance.validVersionUpdated(ValidTermsAndConditions.refreshedNow(termsAndConditions: tac)); + } + @override + Widget build(BuildContext context) { return Scaffold( body: Container( padding: const EdgeInsets.fromLTRB(16, 64, 16, 16), - child: Column( - children: [ - Expanded( - child: Column( + child: Builder( + builder: (context) { + final tacState = context.watch(); + final validTac = tacState.valid; + if (validTac == null) { + // Show spinner if no valid T&C have been resolved yet (not as a result of actually ongoing fetch). + // Should store the future from '_updateValidTac' and use that in a wrapping 'FutureBuilder'..? + return const Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('T&C last checked at $tacLastCheckedAt.'), - Text('Last T&C version accepted: $lastAcceptedVersion'), + CircularProgressIndicator(), + SizedBox(height: 16), + Center(child: Text('Waiting for enforced Terms & Conditions...')), ], - ), - ), - ElevatedButton( - onPressed: () { - state.setTermsAndConditionsLastVerifiedAt(DateTime.fromMillisecondsSinceEpoch(0)); - }, - child: const Text('Reset check time'), - ), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () { - state.sharedPreferences.setTermsAndConditionsAcceptedVersion('0'); - }, - child: const Text('Reset accepted version'), - ), - ], + ); + } + final acceptedTac = tacState.accepted; + if (acceptedTac == null || !acceptedTac.isValid(validTac.termsAndConditions)) { + return TermsAndConditionsScreen( + validTermsAndConditions: validTac.termsAndConditions, + acceptedTermsAndConditionsVersion: acceptedTac?.version, + ); + } + return Column( + children: [ + Expanded( + child: Column( + children: [ + Text('Accepted T&C version: ${tacState.accepted?.version}'), + Text('Valid T&C last refreshed at ${tacState.valid?.refreshedAt}.'), + ], + ), + ), + ElevatedButton( + onPressed: () => context.read().testResetValidTime(), + child: const Text('Reset update time of valid T&C'), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () => context.read().resetValid(), + // NOTE: This breaks the app because no re-fetch is triggered - hot restart is necessary to recover. + child: const Text('Reset valid T&C (and break the app)'), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () { + const tac = AcceptedTermsAndConditions(version: '1.2.3'); + context.read().userAccepted(tac); + }, + child: const Text('Set accepted T&C version to 1.2.3'), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () => context.read().resetAccepted(), + child: const Text('Reset accepted T&C'), + ), + ], + ); + }, ), ), ); diff --git a/lib/screens/terms_and_conditions/refresh_screen.dart b/lib/screens/terms_and_conditions/refresh_screen.dart deleted file mode 100644 index fbab055..0000000 --- a/lib/screens/terms_and_conditions/refresh_screen.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:concordium_wallet/screens/terms_and_conditions/screen.dart'; -import 'package:concordium_wallet/services/wallet_proxy/model.dart'; -import 'package:concordium_wallet/state.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class RefreshTermsAndConditionsScreen extends StatelessWidget { - const RefreshTermsAndConditionsScreen({super.key}); - - void _markCheckPerformed(BuildContext context) { - // Updating timestamp of checking T&C. - context.read().setTermsAndConditionsLastVerifiedAt(DateTime.now()); - } - - @override - Widget build(BuildContext context) { - final state = context.watch(); - final acceptedTacVersion = state.sharedPreferences.termsAndConditionsAcceptedVersion; - - return Scaffold( - body: Container( - padding: const EdgeInsets.fromLTRB(16, 64, 16, 16), - child: FutureBuilder( - future: state.walletProxyService.getTac(), - builder: (context, snapshot) { - final err = snapshot.error; - if (err != null) { - // TODO What to do here? - return Text('Cannot fetch terms and conditions: $err.'); - } - final currentTac = snapshot.data; - // print('currentTac.version=${currentTac?.version}, acceptedTacVersion=$acceptedTacVersion'); - if (currentTac != null) { - // TODO It feels wrong to have this business logic in the widget builder. - if (currentTac.version == acceptedTacVersion) { - // print('already accepted; dismissing'); - // Current T&C is already accepted; update the T&C check time to make the home screen replace this widget. - Future.microtask(() => _markCheckPerformed(context)); - } else { - return TermsAndConditionsScreen( - TermsAndConditionsViewModel( - currentTac, - acceptedTacVersion, - _markCheckPerformed, - ), - ); - } - } - // Loading current T&C version. - return const Center( - child: CircularProgressIndicator(), - ); - }, - ), - ), - ); - } -} diff --git a/lib/screens/terms_and_conditions/screen.dart b/lib/screens/terms_and_conditions/screen.dart index 199e636..5db1a74 100644 --- a/lib/screens/terms_and_conditions/screen.dart +++ b/lib/screens/terms_and_conditions/screen.dart @@ -1,29 +1,16 @@ import 'package:concordium_wallet/screens/terms_and_conditions/widget.dart'; import 'package:concordium_wallet/services/wallet_proxy/model.dart'; -import 'package:concordium_wallet/state.dart'; +import 'package:concordium_wallet/state/terms_and_conditions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -class TermsAndConditionsViewModel { - final TermsAndConditions currentTac; - final String? acceptedTacVersion; - final void Function(BuildContext context) onAccept; - - const TermsAndConditionsViewModel(this.currentTac, this.acceptedTacVersion, this.onAccept); - - void userAccepted(BuildContext context) { - final state = context.read(); - state.sharedPreferences.setTermsAndConditionsAcceptedVersion(currentTac.version); - onAccept(context); - } -} - class TermsAndConditionsScreen extends StatefulWidget { - final TermsAndConditionsViewModel viewModel; + final TermsAndConditions validTermsAndConditions; + final String? acceptedTermsAndConditionsVersion; - const TermsAndConditionsScreen(this.viewModel, {super.key}); + const TermsAndConditionsScreen({super.key, required this.validTermsAndConditions, this.acceptedTermsAndConditionsVersion}); @override State createState() => _TermsAndConditionsScreenState(); @@ -89,7 +76,7 @@ class _TermsAndConditionsScreenState extends State { Flexible( child: GestureDetector( onTap: () { - _launchUrl(widget.viewModel.currentTac.url); + _launchUrl(widget.validTermsAndConditions.url); }, child: RichText( text: TextSpan( @@ -97,16 +84,16 @@ class _TermsAndConditionsScreenState extends State { children: [ const TextSpan(text: 'I have read and agree to the '), TextSpan( - text: 'Terms and Conditions v${widget.viewModel.currentTac.version}', + text: 'Terms and Conditions v${widget.validTermsAndConditions.version}', style: const TextStyle( color: Colors.indigo, fontWeight: FontWeight.bold, ), ), - switch (widget.viewModel.acceptedTacVersion) { + switch (widget.acceptedTermsAndConditionsVersion) { null => const TextSpan(text: '.'), String v => TextSpan( - text: ' (you previously accepted version $v).', + text: ' (you previously accepted v$v).', style: Theme.of(context).textTheme.bodySmall, ), }, @@ -134,7 +121,12 @@ class _TermsAndConditionsScreenState extends State { Function()? _onAcceptButtonPressed(BuildContext context) { if (isAccepted) { - return () => widget.viewModel.userAccepted(context); + return () { + final tac = AcceptedTermsAndConditions( + version: widget.validTermsAndConditions.version, + ); + context.read().userAccepted(tac); + }; } return null; } diff --git a/lib/screens/terms_and_conditions/widget.dart b/lib/screens/terms_and_conditions/widget.dart index 9735d1c..50600fb 100644 --- a/lib/screens/terms_and_conditions/widget.dart +++ b/lib/screens/terms_and_conditions/widget.dart @@ -6,7 +6,9 @@ class ToggleAcceptedWidget extends StatelessWidget { ToggleAcceptedWidget({super.key, required this.isAccepted, required this.setAccepted}); - final MaterialStateProperty icon = MaterialStateProperty.resolveWith((states) => Icon(_iconData(states))); + final MaterialStateProperty icon = MaterialStateProperty.resolveWith( + (states) => Icon(_iconData(states)), + ); static IconData _iconData(Set states) { if (states.contains(MaterialState.selected)) { diff --git a/lib/services/http.dart b/lib/services/http.dart index 0f6cb58..1ae5df2 100644 --- a/lib/services/http.dart +++ b/lib/services/http.dart @@ -1,9 +1,10 @@ import 'package:http/http.dart' as http; class HttpService { - HttpService(); + const HttpService(); Future get(Uri url) async { + // TODO: Implement retry logic. return http.get(url); } } diff --git a/lib/services/shared_preferences/service.dart b/lib/services/shared_preferences/service.dart new file mode 100644 index 0000000..1d49889 --- /dev/null +++ b/lib/services/shared_preferences/service.dart @@ -0,0 +1,19 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPreferencesService { + static const _tacAcceptedVersionKey = 'tac:accepted_version'; + + final SharedPreferences _prefs; + + const SharedPreferencesService(this._prefs); + + String? get termsAndConditionsAcceptedVersion => _prefs.getString(_tacAcceptedVersionKey); + + void setTermsAndConditionsAcceptedVersion(String? v) { + if (v == null) { + _prefs.remove(_tacAcceptedVersionKey); + } else { + _prefs.setString(_tacAcceptedVersionKey, v); + } + } +} diff --git a/lib/services/wallet_proxy/model.dart b/lib/services/wallet_proxy/model.dart index 9473ee8..6f89398 100644 --- a/lib/services/wallet_proxy/model.dart +++ b/lib/services/wallet_proxy/model.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; + part 'model.g.dart'; @JsonSerializable() @@ -6,7 +7,7 @@ class TermsAndConditions { final Uri url; final String version; - TermsAndConditions(this.url, this.version); + const TermsAndConditions(this.url, this.version); factory TermsAndConditions.fromJson(Map json) => _$TermsAndConditionsFromJson(json); diff --git a/lib/services/wallet_proxy/service.dart b/lib/services/wallet_proxy/service.dart index 078a6fa..7313ae6 100644 --- a/lib/services/wallet_proxy/service.dart +++ b/lib/services/wallet_proxy/service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; + import 'package:concordium_wallet/services/http.dart'; import 'package:concordium_wallet/services/wallet_proxy/model.dart'; @@ -14,7 +15,7 @@ enum WalletProxyEndpoint { class WalletProxyConfig { final String baseUrl; - WalletProxyConfig({required this.baseUrl}); + const WalletProxyConfig({required this.baseUrl}); Uri urlOf(WalletProxyEndpoint e) { // We're not worrying about URL encoding of the path @@ -27,10 +28,10 @@ class WalletProxyService { final WalletProxyConfig config; final HttpService httpService; - WalletProxyService({required this.config, required this.httpService}); + const WalletProxyService({required this.config, required this.httpService}); /// Retrieves the terms and conditions from the wallet-proxy. - Future getTac() async { + Future getTermsAndConditions() async { final url = config.urlOf(WalletProxyEndpoint.tacVersion); final response = await httpService.get(url); final jsonResponse = jsonDecode(response.body); diff --git a/lib/state.dart b/lib/state.dart deleted file mode 100644 index e8a6589..0000000 --- a/lib/state.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:concordium_wallet/services/http.dart'; -import 'package:concordium_wallet/services/wallet_proxy/service.dart'; -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class Network { - final WalletProxyConfig walletProxyConfig; - - Network({required this.walletProxyConfig}); -} - -final testnet = Network( - walletProxyConfig: WalletProxyConfig( - baseUrl: 'https://wallet-proxy.testnet.concordium.com', - ), -); - -// TODO: Extend ChangeNotifier? -class AppSharedPreferences { - static const _tacAcceptedVersionKey = 'tac:accepted_version'; - - final SharedPreferences _prefs; - - AppSharedPreferences(this._prefs); - - get termsAndConditionsAcceptedVersion => _prefs.getString(_tacAcceptedVersionKey); - - void setTermsAndConditionsAcceptedVersion(String v) { - _prefs.setString(_tacAcceptedVersionKey, v); - } -} - -class AppState extends ChangeNotifier { - final network = testnet; - final walletProxyService = WalletProxyService( - config: testnet.walletProxyConfig, - httpService: HttpService(), - ); - - var _termsAndConditionsLastVerifiedAt = DateTime.fromMicrosecondsSinceEpoch(0); // force recheck when starting app - - /// The most recent time it was ensured that the currently valid T&C has been accepted. - DateTime get termsAndConditionsLastVerifiedAt => _termsAndConditionsLastVerifiedAt; - - void setTermsAndConditionsLastVerifiedAt(DateTime v) { - _termsAndConditionsLastVerifiedAt = v; - notifyListeners(); - } - - final AppSharedPreferences sharedPreferences; - - AppState(this.sharedPreferences); -} diff --git a/lib/state/config.dart b/lib/state/config.dart new file mode 100644 index 0000000..de75cdc --- /dev/null +++ b/lib/state/config.dart @@ -0,0 +1,12 @@ +import 'package:concordium_wallet/state/network.dart'; + +class Config { + /// All available networks in the app. + final Map availableNetworks; + + const Config({required this.availableNetworks}); + + factory Config.ofNetworks(List networks) { + return Config(availableNetworks: {for (final n in networks) n.name: n}); + } +} diff --git a/lib/state/network.dart b/lib/state/network.dart new file mode 100644 index 0000000..3d5cf16 --- /dev/null +++ b/lib/state/network.dart @@ -0,0 +1,29 @@ +import 'package:concordium_wallet/services/wallet_proxy/service.dart'; +import 'package:flutter/foundation.dart'; + +class NetworkName { + final String name; + + const NetworkName(this.name); + + static const NetworkName testnet = NetworkName('testnet'); + static const NetworkName mainnet = NetworkName('mainnet'); +} + +class Network { + final NetworkName name; + final WalletProxyConfig walletProxyConfig; + + const Network({required this.name, required this.walletProxyConfig}); +} + +class SelectedNetwork extends ChangeNotifier { + Network selected; + + SelectedNetwork(this.selected); + + void setSelected(Network n) { + selected = n; + notifyListeners(); + } +} diff --git a/lib/state/services.dart b/lib/state/services.dart new file mode 100644 index 0000000..4235dd6 --- /dev/null +++ b/lib/state/services.dart @@ -0,0 +1,26 @@ +import 'package:concordium_wallet/services/http.dart'; +import 'package:concordium_wallet/services/shared_preferences/service.dart'; +import 'package:concordium_wallet/services/wallet_proxy/service.dart'; +import 'package:concordium_wallet/state/network.dart'; + +class NetworkServices { + final WalletProxyService walletProxy; + + const NetworkServices({required this.walletProxy}); + + factory NetworkServices.forNetwork(Network n, {required HttpService httpService}) { + return NetworkServices( + walletProxy: WalletProxyService( + config: n.walletProxyConfig, + httpService: httpService, + ), + ); + } +} + +class ServiceRepository { + final Map networkServices; + final SharedPreferencesService sharedPreferences; + + const ServiceRepository({required this.networkServices, required this.sharedPreferences}); +} diff --git a/lib/state/terms_and_conditions.dart b/lib/state/terms_and_conditions.dart new file mode 100644 index 0000000..59482f2 --- /dev/null +++ b/lib/state/terms_and_conditions.dart @@ -0,0 +1,73 @@ +import 'package:concordium_wallet/services/shared_preferences/service.dart'; +import 'package:concordium_wallet/services/wallet_proxy/model.dart'; +import 'package:flutter/foundation.dart'; + +class AcceptedTermsAndConditions { + final String version; + + const AcceptedTermsAndConditions({required this.version}); + + bool isValid(TermsAndConditions tac) { + return version == tac.version; + } +} + +class ValidTermsAndConditions { + final TermsAndConditions termsAndConditions; + final DateTime? refreshedAt; + + const ValidTermsAndConditions({required this.termsAndConditions, required this.refreshedAt}); + + factory ValidTermsAndConditions.refreshedNow({required TermsAndConditions termsAndConditions}) { + return ValidTermsAndConditions(termsAndConditions: termsAndConditions, refreshedAt: DateTime.now()); + } +} + +class TermsAndConditionAcceptance extends ChangeNotifier { + final SharedPreferencesService _prefs; + + AcceptedTermsAndConditions? accepted; + ValidTermsAndConditions? valid; + + TermsAndConditionAcceptance(this._prefs) { + final acceptedVersion = _prefs.termsAndConditionsAcceptedVersion; + if (acceptedVersion != null) { + userAccepted(AcceptedTermsAndConditions(version: acceptedVersion)); + } + } + + void userAccepted(AcceptedTermsAndConditions tac) { + accepted = tac; + _prefs.setTermsAndConditionsAcceptedVersion(tac.version); + notifyListeners(); + } + + void validVersionUpdated(ValidTermsAndConditions tac) { + valid = tac; + notifyListeners(); + } + + void resetAccepted() { + accepted = null; + _prefs.setTermsAndConditionsAcceptedVersion(null); + notifyListeners(); + } + + void resetValid() { + valid = null; + notifyListeners(); + } + + // Temporary - for testing. + void testResetValidTime() { + final valid = this.valid; + if (valid != null) { + validVersionUpdated( + ValidTermsAndConditions( + termsAndConditions: valid.termsAndConditions, + refreshedAt: DateTime.fromMicrosecondsSinceEpoch(0), + ), + ); + } + } +} From c8f68ba1f0fe1c20baacbf864c43ae8161104b5b Mon Sep 17 00:00:00 2001 From: Michael Bisgaard Olesen Date: Sat, 28 Oct 2023 00:35:12 +0200 Subject: [PATCH 2/4] Add doc comments --- lib/main.dart | 2 +- lib/screens/home/screen.dart | 6 ++-- lib/services/http.dart | 5 ++- lib/services/shared_preferences/service.dart | 7 +++- lib/services/wallet_proxy/model.dart | 4 +++ lib/services/wallet_proxy/service.dart | 16 ++++++--- lib/state/config.dart | 19 ++++++++++- lib/state/network.dart | 21 +++++++++--- lib/state/services.dart | 6 ++++ lib/state/terms_and_conditions.dart | 34 +++++++++++++++++--- 10 files changed, 100 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 716f6e1..c114303 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -61,7 +61,7 @@ class App extends StatelessWidget { child: MultiProvider( providers: [ ChangeNotifierProvider( - create: (context) => SelectedNetwork(config.availableNetworks[NetworkName.testnet]!), + create: (context) => ActiveNetwork(config.availableNetworks[NetworkName.testnet]!), ), ChangeNotifierProvider( create: (context) { diff --git a/lib/screens/home/screen.dart b/lib/screens/home/screen.dart index 87cc97e..098b412 100644 --- a/lib/screens/home/screen.dart +++ b/lib/screens/home/screen.dart @@ -20,13 +20,13 @@ class _HomeScreenState extends State { // Fetch currently valid T&C version unconditionally when initializing the widget. final tacAcceptance = context.read(); - final network = context.read(); - final services = context.read().networkServices[network.selected]!; + final network = context.read(); + final services = context.read().networkServices[network.active]!; _updateValidTac(services.walletProxy, tacAcceptance); } static Future _updateValidTac(WalletProxyService walletProxy, TermsAndConditionAcceptance tacAcceptance) async { - final tac = await walletProxy.getTermsAndConditions(); + final tac = await walletProxy.fetchTermsAndConditions(); tacAcceptance.validVersionUpdated(ValidTermsAndConditions.refreshedNow(termsAndConditions: tac)); } diff --git a/lib/services/http.dart b/lib/services/http.dart index 1ae5df2..cbfb7c9 100644 --- a/lib/services/http.dart +++ b/lib/services/http.dart @@ -1,10 +1,13 @@ import 'package:http/http.dart' as http; +/// Service for performing HTTP requests. +/// +/// The service is intended to take care of retry logic, cookie management, etc. class HttpService { const HttpService(); + /// Performs an HTTP request asynchronously. Future get(Uri url) async { - // TODO: Implement retry logic. return http.get(url); } } diff --git a/lib/services/shared_preferences/service.dart b/lib/services/shared_preferences/service.dart index 1d49889..8c5a07f 100644 --- a/lib/services/shared_preferences/service.dart +++ b/lib/services/shared_preferences/service.dart @@ -1,15 +1,20 @@ import 'package:shared_preferences/shared_preferences.dart'; +/// Service for interacting with [SharedPreferences]. class SharedPreferencesService { + /// String key associated with the persisted accepted T&C version. static const _tacAcceptedVersionKey = 'tac:accepted_version'; + /// Wrapped instance. final SharedPreferences _prefs; const SharedPreferencesService(this._prefs); + /// Currently accepted shared preferences. String? get termsAndConditionsAcceptedVersion => _prefs.getString(_tacAcceptedVersionKey); - void setTermsAndConditionsAcceptedVersion(String? v) { + /// Sets or deletes the currently accepted shared preferences. + void updateTermsAndConditionsAcceptedVersion(String? v) { if (v == null) { _prefs.remove(_tacAcceptedVersionKey); } else { diff --git a/lib/services/wallet_proxy/model.dart b/lib/services/wallet_proxy/model.dart index 6f89398..d5aea77 100644 --- a/lib/services/wallet_proxy/model.dart +++ b/lib/services/wallet_proxy/model.dart @@ -1,10 +1,14 @@ +import 'package:concordium_wallet/services/wallet_proxy/service.dart'; import 'package:json_annotation/json_annotation.dart'; part 'model.g.dart'; +/// Response from endpoint [WalletProxyEndpoint.termsAndConditionsVersion]. @JsonSerializable() class TermsAndConditions { + /// URL of the T&C text. final Uri url; + /// T&C version. final String version; const TermsAndConditions(this.url, this.version); diff --git a/lib/services/wallet_proxy/service.dart b/lib/services/wallet_proxy/service.dart index 7313ae6..686f9ed 100644 --- a/lib/services/wallet_proxy/service.dart +++ b/lib/services/wallet_proxy/service.dart @@ -3,8 +3,9 @@ import 'dart:convert'; import 'package:concordium_wallet/services/http.dart'; import 'package:concordium_wallet/services/wallet_proxy/model.dart'; +/// Paths of the wallet-proxy endpoints. enum WalletProxyEndpoint { - tacVersion('v0/termsAndConditionsVersion'), + termsAndConditionsVersion('v0/termsAndConditionsVersion'), ; final String path; @@ -12,7 +13,11 @@ enum WalletProxyEndpoint { const WalletProxyEndpoint(this.path); } +/// Configuration to control the interaction with a wallet-proxy instance. class WalletProxyConfig { + /// Base URL of the instance. + /// + /// Endpoint URLs are constructed by simple concatenation of this value and the endpoint path. final String baseUrl; const WalletProxyConfig({required this.baseUrl}); @@ -24,15 +29,18 @@ class WalletProxyConfig { } } +/// Service for interacting with a wallet-proxy instance. class WalletProxyService { + /// Configuration of the instance. final WalletProxyConfig config; + /// HTTP service used to send requests to the instance. final HttpService httpService; const WalletProxyService({required this.config, required this.httpService}); - /// Retrieves the terms and conditions from the wallet-proxy. - Future getTermsAndConditions() async { - final url = config.urlOf(WalletProxyEndpoint.tacVersion); + /// Fetches the currently valid T&C. + Future fetchTermsAndConditions() async { + final url = config.urlOf(WalletProxyEndpoint.termsAndConditionsVersion); final response = await httpService.get(url); final jsonResponse = jsonDecode(response.body); return TermsAndConditions.fromJson(jsonResponse); diff --git a/lib/state/config.dart b/lib/state/config.dart index de75cdc..05d3765 100644 --- a/lib/state/config.dart +++ b/lib/state/config.dart @@ -1,7 +1,24 @@ import 'package:concordium_wallet/state/network.dart'; +/// Global configuration of the app. +/// +/// For now, the configuration is hardcoded and only contains the network configuration of each available network. +/// +/// The intent is that it should be loaded from a predefined location that is set with a build parameter +/// and include all necessary information for setting up the services used by the app. +/// This means things like where to find IP info, T&C, forced upgrade config, news, marketplace apps, etc. +/// +/// Custom configuration exposed to the user is not intended to live in this object. class Config { - /// All available networks in the app. + /// All available networks and their configuration. + /// + /// It will eventually be possible for the user to manage the set of available networks + /// at which point the source of truth will live somewhere else (with this value being the default). + /// + /// At some other point we'll introduce a notion of "enabled" networks, + /// i.e. the list of networks to be included in the user's network selector. + /// That list will be a subset of the available networks. + /// The purpose of describing the concept here already is to allow other doc comments to reference it early. final Map availableNetworks; const Config({required this.availableNetworks}); diff --git a/lib/state/network.dart b/lib/state/network.dart index 3d5cf16..895c227 100644 --- a/lib/state/network.dart +++ b/lib/state/network.dart @@ -1,29 +1,40 @@ +import 'package:concordium_wallet/state/config.dart'; import 'package:concordium_wallet/services/wallet_proxy/service.dart'; import 'package:flutter/foundation.dart'; +/// Name of a network. class NetworkName { final String name; const NetworkName(this.name); + /// Standard name of the testnet network. static const NetworkName testnet = NetworkName('testnet'); + /// Standard name of the mainnet network. static const NetworkName mainnet = NetworkName('mainnet'); } +/// Configuration of all services of a specific network. class Network { + /// Name of the network. final NetworkName name; + /// Configuration of the Wallet Proxy service belonging to the network. final WalletProxyConfig walletProxyConfig; const Network({required this.name, required this.walletProxyConfig}); } -class SelectedNetwork extends ChangeNotifier { - Network selected; +/// State component acting as the source of truth for what network is currently active in the app. +class ActiveNetwork extends ChangeNotifier { + /// Currently active network. + /// + /// The network is guaranteed to be one of the "enabled" networks (as defined in [Config.availableNetworks]). + Network active; - SelectedNetwork(this.selected); + ActiveNetwork(this.active); - void setSelected(Network n) { - selected = n; + void setActive(Network n) { + active = n; notifyListeners(); } } diff --git a/lib/state/services.dart b/lib/state/services.dart index 4235dd6..e4aa6d6 100644 --- a/lib/state/services.dart +++ b/lib/state/services.dart @@ -1,9 +1,12 @@ import 'package:concordium_wallet/services/http.dart'; import 'package:concordium_wallet/services/shared_preferences/service.dart'; import 'package:concordium_wallet/services/wallet_proxy/service.dart'; +import 'package:concordium_wallet/state/config.dart'; import 'package:concordium_wallet/state/network.dart'; +/// Collection of all services of a network. class NetworkServices { + /// The Wallet Proxy service on the network. final WalletProxyService walletProxy; const NetworkServices({required this.walletProxy}); @@ -18,8 +21,11 @@ class NetworkServices { } } +/// Collection of all services available to the app. class ServiceRepository { + /// Service collections for all "enabled" networks (as defined in [Config.availableNetworks]). final Map networkServices; + /// Global service for interacting with shared preferences. final SharedPreferencesService sharedPreferences; const ServiceRepository({required this.networkServices, required this.sharedPreferences}); diff --git a/lib/state/terms_and_conditions.dart b/lib/state/terms_and_conditions.dart index 59482f2..48d9e70 100644 --- a/lib/state/terms_and_conditions.dart +++ b/lib/state/terms_and_conditions.dart @@ -2,31 +2,47 @@ import 'package:concordium_wallet/services/shared_preferences/service.dart'; import 'package:concordium_wallet/services/wallet_proxy/model.dart'; import 'package:flutter/foundation.dart'; +/// Version of the Terms & Conditions accepted by the user. class AcceptedTermsAndConditions { + /// Accepted version. final String version; const AcceptedTermsAndConditions({required this.version}); + /// Whether the accepted version is valid with respect to the provided valid version. bool isValid(TermsAndConditions tac) { return version == tac.version; } } +/// Version of the Terms & Conditions that is considered valid. +/// +/// The user has to have accepted this version (or more generally, a compatible version) +/// for the acceptance to be valid. class ValidTermsAndConditions { + /// T&C configuration fetched from an external endpoint. final TermsAndConditions termsAndConditions; - final DateTime? refreshedAt; + /// Latest time at which [termsAndConditions] is known to be valid. + final DateTime refreshedAt; const ValidTermsAndConditions({required this.termsAndConditions, required this.refreshedAt}); + /// Constructs an instance for the provided [TermsAndConditions] with a refresh time of the current time. factory ValidTermsAndConditions.refreshedNow({required TermsAndConditions termsAndConditions}) { return ValidTermsAndConditions(termsAndConditions: termsAndConditions, refreshedAt: DateTime.now()); } } +/// State component of the currently accepted and valid Terms & Conditions. class TermsAndConditionAcceptance extends ChangeNotifier { + /// Service used to persist the accepted T&C version. final SharedPreferencesService _prefs; + /// Currently accepted T&C. + /// + /// The accepted version is persisted into shared preferences. AcceptedTermsAndConditions? accepted; + /// Currently valid T&C. ValidTermsAndConditions? valid; TermsAndConditionAcceptance(this._prefs) { @@ -36,29 +52,39 @@ class TermsAndConditionAcceptance extends ChangeNotifier { } } + /// Update the currently accepted T&C and persist the new value. + /// + /// Use [resetAccepted] to revoke acceptance. void userAccepted(AcceptedTermsAndConditions tac) { accepted = tac; - _prefs.setTermsAndConditionsAcceptedVersion(tac.version); + _prefs.updateTermsAndConditionsAcceptedVersion(tac.version); notifyListeners(); } + /// Updates the currently valid T&C. void validVersionUpdated(ValidTermsAndConditions tac) { valid = tac; notifyListeners(); } + /// Revokes T&C acceptance and delete it from persistence. void resetAccepted() { accepted = null; - _prefs.setTermsAndConditionsAcceptedVersion(null); + _prefs.updateTermsAndConditionsAcceptedVersion(null); notifyListeners(); } + /// Resets the valid T&C. + /// + /// This should trigger a reload and re-verification of the validity of the acceptance. void resetValid() { valid = null; notifyListeners(); } - // Temporary - for testing. + /// Resets the update time of the currently valid T&C (if present). + /// + /// This method is not likely to have any uses besides maybe testing. void testResetValidTime() { final valid = this.valid; if (valid != null) { From df06a51c9f682f69ec6793399eab146f7b085fe3 Mon Sep 17 00:00:00 2001 From: Michael Bisgaard Olesen Date: Sun, 29 Oct 2023 23:32:34 +0100 Subject: [PATCH 3/4] Fmt --- lib/services/wallet_proxy/model.dart | 1 + lib/services/wallet_proxy/service.dart | 1 + lib/state/config.dart | 8 +++++--- lib/state/network.dart | 2 ++ lib/state/services.dart | 1 + lib/state/terms_and_conditions.dart | 2 ++ 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/services/wallet_proxy/model.dart b/lib/services/wallet_proxy/model.dart index d5aea77..d1ca958 100644 --- a/lib/services/wallet_proxy/model.dart +++ b/lib/services/wallet_proxy/model.dart @@ -8,6 +8,7 @@ part 'model.g.dart'; class TermsAndConditions { /// URL of the T&C text. final Uri url; + /// T&C version. final String version; diff --git a/lib/services/wallet_proxy/service.dart b/lib/services/wallet_proxy/service.dart index 686f9ed..a5e6c3a 100644 --- a/lib/services/wallet_proxy/service.dart +++ b/lib/services/wallet_proxy/service.dart @@ -33,6 +33,7 @@ class WalletProxyConfig { class WalletProxyService { /// Configuration of the instance. final WalletProxyConfig config; + /// HTTP service used to send requests to the instance. final HttpService httpService; diff --git a/lib/state/config.dart b/lib/state/config.dart index 05d3765..8bfcfd0 100644 --- a/lib/state/config.dart +++ b/lib/state/config.dart @@ -1,14 +1,16 @@ import 'package:concordium_wallet/state/network.dart'; /// Global configuration of the app. -/// +/// /// For now, the configuration is hardcoded and only contains the network configuration of each available network. -/// +/// /// The intent is that it should be loaded from a predefined location that is set with a build parameter /// and include all necessary information for setting up the services used by the app. /// This means things like where to find IP info, T&C, forced upgrade config, news, marketplace apps, etc. +/// and maybe even behavioral parameters like refresh frequency of account balance and other kinds of policies. /// -/// Custom configuration exposed to the user is not intended to live in this object. +/// The class is immutable as it just models configuration that is defined somewhere else. +/// In particular, user configuration is not intended to live in this class. class Config { /// All available networks and their configuration. /// diff --git a/lib/state/network.dart b/lib/state/network.dart index 895c227..96f3c0e 100644 --- a/lib/state/network.dart +++ b/lib/state/network.dart @@ -10,6 +10,7 @@ class NetworkName { /// Standard name of the testnet network. static const NetworkName testnet = NetworkName('testnet'); + /// Standard name of the mainnet network. static const NetworkName mainnet = NetworkName('mainnet'); } @@ -18,6 +19,7 @@ class NetworkName { class Network { /// Name of the network. final NetworkName name; + /// Configuration of the Wallet Proxy service belonging to the network. final WalletProxyConfig walletProxyConfig; diff --git a/lib/state/services.dart b/lib/state/services.dart index e4aa6d6..8756fcd 100644 --- a/lib/state/services.dart +++ b/lib/state/services.dart @@ -25,6 +25,7 @@ class NetworkServices { class ServiceRepository { /// Service collections for all "enabled" networks (as defined in [Config.availableNetworks]). final Map networkServices; + /// Global service for interacting with shared preferences. final SharedPreferencesService sharedPreferences; diff --git a/lib/state/terms_and_conditions.dart b/lib/state/terms_and_conditions.dart index 48d9e70..b1c521f 100644 --- a/lib/state/terms_and_conditions.dart +++ b/lib/state/terms_and_conditions.dart @@ -22,6 +22,7 @@ class AcceptedTermsAndConditions { class ValidTermsAndConditions { /// T&C configuration fetched from an external endpoint. final TermsAndConditions termsAndConditions; + /// Latest time at which [termsAndConditions] is known to be valid. final DateTime refreshedAt; @@ -42,6 +43,7 @@ class TermsAndConditionAcceptance extends ChangeNotifier { /// /// The accepted version is persisted into shared preferences. AcceptedTermsAndConditions? accepted; + /// Currently valid T&C. ValidTermsAndConditions? valid; From 3737e8498403a69a44e576bac94273c720b45f1e Mon Sep 17 00:00:00 2001 From: Michael Bisgaard Olesen Date: Mon, 30 Oct 2023 11:39:41 +0100 Subject: [PATCH 4/4] iOS: Add/update project files (#13) The previous PR was not built on a Mac and therefore didn't include the updated iOS project files. This change is the result of running `flutter pub get` on a Mac and ensures that the files are in sync. Going forward, we'll need a CI workflow to check that the project files are in sync on all platforms (i.e. that `flutter pub get` doesn't change any files). This may be one that triggers only on changes to `pubspec.yaml` of the one that runs tests (see task https://concordium.atlassian.net/browse/CBW-1482). --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 ++++++ ios/Podfile.lock | 29 ++++ ios/Runner.xcodeproj/project.pbxproj | 138 ++++++++++++++++-- .../contents.xcworkspacedata | 3 + 6 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 ios/Podfile create mode 100644 ios/Podfile.lock diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..a9bf442 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - Flutter (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + +PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 + +COCOAPODS: 1.12.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5d2aa1d..1dc7ee5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,12 +8,14 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + D5CE9A053BAA3ECA9CF91AA4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C7FA29C7A98E7F3A33532FE /* Pods_RunnerTests.framework */; }; + EC23DE7A62A94BA4B34C5271 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4C484F98A8A8CCE8BBA8AF9 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,9 +42,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0F99E43068C5CAE102BBC2DD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6A4F7753BCBF5231792C7425 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -53,8 +59,12 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C7FA29C7A98E7F3A33532FE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A4C484F98A8A8CCE8BBA8AF9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BC6FD34E1586DE99650CE010 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DD2B784F3F8F542408FB3BF7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E757F1E51237464C6D383139 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + E8B5F6D4AEF91C08CD67541F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,12 +72,29 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EC23DE7A62A94BA4B34C5271 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FFE7C37E3C848BBE556D56DF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D5CE9A053BAA3ECA9CF91AA4 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -79,14 +106,6 @@ name = Flutter; sourceTree = ""; }; - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( @@ -94,6 +113,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + E04461A86F8CDA20109DE6D5 /* Pods */, + F3380709E22A1A088F8C6961 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +142,29 @@ path = Runner; sourceTree = ""; }; + E04461A86F8CDA20109DE6D5 /* Pods */ = { + isa = PBXGroup; + children = ( + 0F99E43068C5CAE102BBC2DD /* Pods-Runner.debug.xcconfig */, + BC6FD34E1586DE99650CE010 /* Pods-Runner.release.xcconfig */, + E8B5F6D4AEF91C08CD67541F /* Pods-Runner.profile.xcconfig */, + DD2B784F3F8F542408FB3BF7 /* Pods-RunnerTests.debug.xcconfig */, + 6A4F7753BCBF5231792C7425 /* Pods-RunnerTests.release.xcconfig */, + E757F1E51237464C6D383139 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + F3380709E22A1A088F8C6961 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A4C484F98A8A8CCE8BBA8AF9 /* Pods_Runner.framework */, + 9C7FA29C7A98E7F3A33532FE /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,9 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 9562DF1184D78C05217C1F50 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, - 331C807E294A63A400263BE5 /* Frameworks */, 331C807F294A63A400263BE5 /* Resources */, + FFE7C37E3C848BBE556D56DF /* Frameworks */, ); buildRules = ( ); @@ -146,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 8EF006C1CC89F341448A3566 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 95CFCC754F8CD12608F07242 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -239,6 +286,67 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 8EF006C1CC89F341448A3566 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9562DF1184D78C05217C1F50 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 95CFCC754F8CD12608F07242 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -377,7 +485,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = DD2B784F3F8F542408FB3BF7 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -395,7 +503,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 6A4F7753BCBF5231792C7425 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,7 +519,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = E757F1E51237464C6D383139 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + +