diff --git a/.gitignore b/.gitignore index d8b2cd99..8a3f2db5 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,5 @@ app.*.map.json /android/app/profile /android/app/release -# API secrets -*.env \ No newline at end of file +# env variables and secrets +*.env diff --git a/lib/app.dart b/lib/app.dart index d497ccca..ce3e946f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -15,6 +15,8 @@ import 'package:hypha_wallet/ui/blocs/authentication/authentication_bloc.dart'; import 'package:hypha_wallet/ui/blocs/deeplink/deeplink_bloc.dart'; import 'package:hypha_wallet/ui/blocs/error_handler/error_handler_bloc.dart'; import 'package:hypha_wallet/ui/blocs/push_notifications/push_notifications_bloc.dart'; +import 'package:hypha_wallet/ui/bottom_navigation/hypha_bottom_navigation.dart'; +import 'package:hypha_wallet/ui/onboarding/onboarding_page.dart'; import 'package:hypha_wallet/ui/onboarding/onboarding_page_with_link.dart'; import 'package:hypha_wallet/ui/settings/hypha_confirmation_page.dart'; import 'package:hypha_wallet/ui/settings/interactor/settings_bloc.dart'; @@ -61,15 +63,16 @@ class HyphaAppView extends StatelessWidget { return previous.authStatus != current.authStatus; }, listener: (context, state) { + LogHelper.d('Auth Bloc Listener FIRED'); switch (state.authStatus) { case Unknown _: LogHelper.d('Auth Bloc Listener unknown'); break; case Authenticated _: - Get.offAll(() => const SplashScreen(isAuthenticated: true)); + Get.offAll(() => const HyphaBottomNavigation()); break; case UnAuthenticated _: - Get.offAll(() => const SplashScreen(isAuthenticated: false)); + Get.offAll(() => const OnboardingPage()); break; } }, @@ -188,7 +191,7 @@ class HyphaAppView extends StatelessWidget { theme: HyphaTheme.lightTheme, themeMode: state.themeMode, navigatorObservers: [GetIt.I.get().firebaseObserver], - home: const SizedBox.shrink(), + home: const SplashPage(), ); }, ), diff --git a/lib/core/di/di_setup.dart b/lib/core/di/di_setup.dart index a628f8f5..5edd56cb 100644 --- a/lib/core/di/di_setup.dart +++ b/lib/core/di/di_setup.dart @@ -25,6 +25,7 @@ import 'package:hypha_wallet/core/network/api/services/sign_transaction_callback import 'package:hypha_wallet/core/network/api/services/token_service.dart'; import 'package:hypha_wallet/core/network/api/services/transaction_history_service.dart'; import 'package:hypha_wallet/core/network/api/services/user_account_service.dart'; +import 'package:hypha_wallet/core/network/ipfs/ipfs_manager.dart'; import 'package:hypha_wallet/core/network/models/network.dart'; import 'package:hypha_wallet/core/network/models/user_profile_data.dart'; import 'package:hypha_wallet/core/network/networking_manager.dart'; diff --git a/lib/core/di/services_module.dart b/lib/core/di/services_module.dart index 29208633..f42f71d8 100644 --- a/lib/core/di/services_module.dart +++ b/lib/core/di/services_module.dart @@ -10,6 +10,7 @@ Future _registerServicesModule() async { // TODO(n13): Only remaining hard-coded reference to Telos - I guess we can't create NetworkingManager with base URL since the base URL depends on the network? _registerLazySingleton(() => NetworkingManager(_getIt().baseUrl(network: Network.telos))); + _registerLazySingleton(() => IPFSManager()); /// Secure Storage _registerLazySingleton(() => const FlutterSecureStorage()); diff --git a/lib/core/network/ipfs/ipfs_manager.dart b/lib/core/network/ipfs/ipfs_manager.dart new file mode 100644 index 00000000..575d0346 --- /dev/null +++ b/lib/core/network/ipfs/ipfs_manager.dart @@ -0,0 +1,17 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:ipfs_client_flutter/ipfs_client_flutter.dart'; + +class IPFSManager { + final serverUlr = 'https://ipfs.infura.io:5001'; + late IpfsClient ipfsClient; + + IPFSManager() { + final apiKeySecret = dotenv.env['IPFS_API_KEY_SECRET']; + ipfsClient = IpfsClient(url: serverUlr, authorizationToken: apiKeySecret); + } + + Future getImage(String imageToken) async { + final response = await ipfsClient.read(dir: imageToken); + return response; + } +} diff --git a/lib/core/network/models/dao_data_model.dart b/lib/core/network/models/dao_data_model.dart index b0a53649..ce1505b4 100644 --- a/lib/core/network/models/dao_data_model.dart +++ b/lib/core/network/models/dao_data_model.dart @@ -2,24 +2,28 @@ class DaoData { final String docId; final String detailsDaoName; final String settingsDaoTitle; - final String settingsLogo; + final String logoIPFSHash; + final String logoType; final String settingsDaoUrl; DaoData({ required this.docId, required this.detailsDaoName, required this.settingsDaoTitle, - required this.settingsLogo, + required this.logoIPFSHash, + required this.logoType, required this.settingsDaoUrl, }); factory DaoData.fromJson(Map json) { final Map settings = json['settings'][0]; + final logoUrlAndType = settings['settings_logo_s']?.split(':') ?? ['', '']; return DaoData( docId: json['docId'], detailsDaoName: json['details_daoName_n'], settingsDaoTitle: settings['settings_daoTitle_s'], - settingsLogo: settings['settings_logo_s'] ?? '', + logoIPFSHash: logoUrlAndType[0], + logoType: logoUrlAndType[1], settingsDaoUrl: settings['settings_daoUrl_s'] ?? '', ); } diff --git a/lib/core/network/networking_manager.dart b/lib/core/network/networking_manager.dart index ba461b77..2abbdb7e 100644 --- a/lib/core/network/networking_manager.dart +++ b/lib/core/network/networking_manager.dart @@ -23,18 +23,17 @@ class NetworkingManager extends DioForNative { final loggerInterceptor = PrettyDioLogger( requestHeader: false, requestBody: true, - responseBody: true, + responseBody: false, responseHeader: false, error: true, compact: true, ); + interceptors.add(retryInterceptor); if (_isDebugNetworking) { interceptors.add(loggerInterceptor); } - interceptors.add(retryInterceptor); - options.connectTimeout = Endpoints.connectionTimeout; options.receiveTimeout = Endpoints.receiveTimeout; options.responseType = ResponseType.json; diff --git a/lib/core/network/repository/auth_repository.dart b/lib/core/network/repository/auth_repository.dart index 28834194..87a756c6 100644 --- a/lib/core/network/repository/auth_repository.dart +++ b/lib/core/network/repository/auth_repository.dart @@ -29,7 +29,7 @@ class AuthRepository { final FirebaseDatabaseService _firebaseDatabaseService; final StreamController _controller = StreamController.broadcast(); - Authenticated? authenticateUser; + AuthenticationStatus currentAuthStatus = const Unknown(); AuthRepository( this._appSharedPrefs, @@ -42,7 +42,7 @@ class AuthRepository { ) { status.listen((AuthenticationStatus event) { if (event is Authenticated) { - authenticateUser = event; + currentAuthStatus = event; } }); } @@ -101,9 +101,9 @@ class AuthRepository { } /// Use this method when we expect the auth data to be there. Anytime after auth. If the data isnt there. then crash - Authenticated get authDataOrCrash { - if (authenticateUser is Authenticated) { - return authenticateUser!; + Authenticated get authDataOrCrash { + if (currentAuthStatus is Authenticated) { + return currentAuthStatus as Authenticated; } throw Exception('Attempted to fetch Auth data but the user is not authenticated. '); diff --git a/lib/main.dart b/lib/main.dart index 5c2c3f95..708578ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hypha_wallet/app.dart'; import 'package:hypha_wallet/core/di/di_setup.dart'; import 'package:hypha_wallet/ui/blocs/bloc_observer.dart'; @@ -10,6 +11,7 @@ void main() async { // Initialize Flutter WidgetsFlutterBinding.ensureInitialized(); + await dotenv.load(fileName: '.env'); await setupDependencies(); if (kDebugMode) { diff --git a/lib/ui/profile/components/dao_widget.dart b/lib/ui/profile/components/dao_widget.dart index 70cbcc44..2d41ad48 100644 --- a/lib/ui/profile/components/dao_widget.dart +++ b/lib/ui/profile/components/dao_widget.dart @@ -1,11 +1,48 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; import 'package:hypha_wallet/design/hypha_card.dart'; +import 'package:hypha_wallet/design/hypha_colors.dart'; import 'package:hypha_wallet/design/themes/extensions/theme_extension_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// Render an IPFS image +/// +/// Images come in the form ':type' - we use type to render correctly +/// +/// Supports svg and bitmap images +/// +class IpfsImage extends StatelessWidget { + final String ipfsHash; + final String type; + + late final String url = 'https://ipfs.io/ipfs/$ipfsHash'; + + IpfsImage({super.key, required this.ipfsHash, required this.type}); + + @override + Widget build(BuildContext context) { + switch (type) { + case 'svg': + return SvgPicture.network(url); + case 'png': + case 'jpg': + case 'jpeg': + case 'gif': + case 'bmp': + case 'webp': + return Image.network(url); + default: + return const Icon(Icons.error, color: Colors.red); // Default error icon in case the format isn't supported. + } + } +} class DaoWidget extends StatelessWidget { final DaoData dao; + String get daoUrl => 'https://dao.hypha.earth/${dao.settingsDaoUrl}'; + const DaoWidget({ super.key, required this.dao, @@ -13,54 +50,87 @@ class DaoWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 22), - child: HyphaCard( - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () {}, - child: Padding( - padding: const EdgeInsets.only(left: 22, right: 22, top: 12, bottom: 22), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - dense: true, - // leading: FutureBuilder( - // future: fetchSVGFromIPFS('QmV3KmaoqCCXuCDvHzYWS9Jg3RfjrDTQSXK1e7453qfSRS'), - // builder: (context, snapshot) { - // if (snapshot.connectionState == ConnectionState.waiting) { - // return CircularProgressIndicator(); - // } else if (snapshot.hasError) { - // return Text('Error: ${snapshot.error}'); - // } else { - // return SvgPicture.string(snapshot.data!); - // } - // }, - // ), - visualDensity: VisualDensity.compact, - title: Text(dao.settingsDaoTitle, style: context.hyphaTextTheme.smallTitles), - ), - // const SizedBox(height: 14), - // const HyphaDivider(), - // const SizedBox(height: 20), - ], + return HyphaCard( + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + if (!await launchUrl(Uri.parse(daoUrl), mode: LaunchMode.externalApplication)) { + throw Exception('Could not launch $daoUrl'); + } + }, + child: Stack( + children: [ + const Positioned( + right: 12, + top: 12, + child: Icon(Icons.navigate_next), ), - ), + Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: + Colors.white, // Note: white bg is standard on the DAO website where people upload images + ), + child: ClipOval( + child: IpfsImage( + ipfsHash: dao.logoIPFSHash, + type: dao.logoType, + ), + ), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(dao.settingsDaoTitle, style: context.hyphaTextTheme.smallTitles), + Text( + 'dao.hypha.earth/${dao.settingsDaoUrl}', + style: context.hyphaTextTheme.reducedTitles.copyWith(color: HyphaColors.primaryBlu), + ), + ], + ) + ], + ), + // const SizedBox(height: 12), + // const HyphaDivider(), + // const SizedBox(height: 12), + // Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Row( + // children: [ + // const Icon(Icons.calendar_month), + // const SizedBox(width: 4), + // Text( + // 'The date here', + // style: context.hyphaTextTheme.ralMediumSmallNote.copyWith( + // height: 0, + // color: HyphaColors.midGrey, + // ), + // ), + // ], + // ), + // Text('Core member', style: context.hyphaTextTheme.ralMediumBody.copyWith(height: 0)), + // ], + // ) + ], + ), + ), + ], ), ), ); } } - -// Future fetchSVGFromIPFS(String ipfsHash) async { -// final ipfsURL = 'https://ipfs.io/ipfs/$ipfsHash:svg'; -// final response = await http.get(Uri.parse(ipfsURL)); -// -// if (response.statusCode == 200) { -// return response.body; -// } else { -// throw Exception('Failed to fetch SVG from IPFS. Status code: ${response.statusCode}'); -// } -// } diff --git a/lib/ui/profile/components/profile_view.dart b/lib/ui/profile/components/profile_view.dart index e7867e53..5b8f9906 100644 --- a/lib/ui/profile/components/profile_view.dart +++ b/lib/ui/profile/components/profile_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hypha_wallet/core/network/models/dao_data_model.dart'; import 'package:hypha_wallet/design/avatar_image/hypha_avatar_image.dart'; import 'package:hypha_wallet/design/background/hypha_half_background.dart'; import 'package:hypha_wallet/design/background/hypha_page_background.dart'; @@ -13,6 +14,7 @@ import 'package:hypha_wallet/ui/profile/components/profile_edit_menu_bottom_shee import 'package:hypha_wallet/ui/profile/interactor/profile_bloc.dart'; import 'package:hypha_wallet/ui/shared/hypha_body_widget.dart'; import 'package:hypha_wallet/ui/shared/hypha_error_view.dart'; +import 'package:hypha_wallet/ui/shared/listview_with_all_separators.dart'; class ProfileView extends StatelessWidget { const ProfileView({super.key}); @@ -119,9 +121,7 @@ class ProfileView extends StatelessWidget { onChanged: (value) {}, ), ], - Wrap( - children: state.profileData?.daos.map((e) => DaoWidget(dao: e)).toList() ?? [], - ), + DaosView(daos: state.profileData?.daos ?? []), ], ), ], @@ -134,3 +134,41 @@ class ProfileView extends StatelessWidget { ); } } + +class DaosView extends StatelessWidget { + final List daos; + + const DaosView({super.key, required this.daos}); + + @override + Widget build(BuildContext context) { + if (daos.isEmpty) { + return const SizedBox.shrink(); + } else { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + 'My DAOs', + style: context.hyphaTextTheme.ralMediumBody.copyWith( + color: HyphaColors.midGrey, + ), + ), + ), + ListViewWithAllSeparators( + shrinkWrap: true, + items: daos, + itemBuilder: (_, DaoData dao, __) { + return DaoWidget(dao: dao); + }, + separatorBuilder: (_, __) => const SizedBox(height: 12)) + ], + ), + ); + } + } +} diff --git a/lib/ui/splash/splash_page.dart b/lib/ui/splash/splash_page.dart index a954c41e..8af18d38 100644 --- a/lib/ui/splash/splash_page.dart +++ b/lib/ui/splash/splash_page.dart @@ -1,34 +1,40 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hypha_wallet/core/logging/log_helper.dart'; +import 'package:hypha_wallet/core/network/repository/auth_repository.dart'; import 'package:hypha_wallet/ui/bottom_navigation/hypha_bottom_navigation.dart'; import 'package:hypha_wallet/ui/onboarding/onboarding_page.dart'; import 'package:lottie/lottie.dart'; -class SplashScreen extends StatefulWidget { - /// If true, means we are authenticated and navigate to bottom page: Else navigate to onboarding. - final bool isAuthenticated; - - const SplashScreen({super.key, required this.isAuthenticated}); +class SplashPage extends StatefulWidget { + const SplashPage({super.key}); @override - _SplashScreenState createState() => _SplashScreenState(); + _SplashPageState createState() => _SplashPageState(); } -class _SplashScreenState extends State with TickerProviderStateMixin { +class _SplashPageState extends State with TickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( - duration: const Duration(seconds: 5), vsync: this, ); } + @override + void dispose() { + _controller.dispose(); // you need this + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( + extendBodyBehindAppBar: true, body: Lottie.asset( 'assets/animations/hypha_splash.json', controller: _controller, @@ -37,9 +43,18 @@ class _SplashScreenState extends State with TickerProviderStateMix onLoaded: (composition) { _controller ..duration = composition.duration - ..forward().whenComplete(() => Get.offAll( - widget.isAuthenticated ? const HyphaBottomNavigation() : const OnboardingPage(), - )); + ..forward().whenComplete(() { + final userAuthData = GetIt.I.get().currentAuthStatus; + if (userAuthData is Authenticated) { + if (Get.currentRoute != '/HyphaBottomNavigation') { + Get.offAll(const HyphaBottomNavigation()); + } + } else { + if (Get.currentRoute != '/OnboardingPage') { + Get.offAll(const OnboardingPage()); + } + } + }); }, ), ); diff --git a/pubspec.lock b/pubspec.lock index c605a46b..df3d3f84 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -518,6 +518,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -800,6 +808,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + ipfs_client_flutter: + dependency: "direct main" + description: + name: ipfs_client_flutter + sha256: e9ec8c3b33af20240cdfcb1316ef0804be68d5237df65a82ff3f3a2cfc2fd497 + url: "https://pub.dev" + source: hosted + version: "1.0.7" js: dependency: transitive description: @@ -1337,10 +1353,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.1.12" url_launcher_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 60f15e8e..de737d94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,7 +101,7 @@ dependencies: share_plus: ^7.0.0 # Launch URL - url_launcher: ^6.1.10 + url_launcher: ^6.1.12 # Fonts google_fonts: ^4.0.4 @@ -139,6 +139,15 @@ dependencies: # Animations lottie: ^2.5.0 + # Load images and files from IPFS + ipfs_client_flutter: ^1.0.7 + + # Env variables + flutter_dotenv: ^5.1.0 + +dependency_overrides: + dio: ^5.3.0 + dev_dependencies: flutter_test: sdk: flutter @@ -192,6 +201,7 @@ flutter: - assets/images/logos/dark/hypha_logo_composite.svg - assets/config/profile_service/config.json - assets/animations/hypha_splash.json + - .env # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware