Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch image #229

Merged
merged 19 commits into from
Aug 12, 2023
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ app.*.map.json
/android/app/profile
/android/app/release

# API secrets
*.env
# env variables and secrets
*.env
9 changes: 6 additions & 3 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
},
Expand Down Expand Up @@ -188,7 +191,7 @@ class HyphaAppView extends StatelessWidget {
theme: HyphaTheme.lightTheme,
themeMode: state.themeMode,
navigatorObservers: <NavigatorObserver>[GetIt.I.get<FirebaseAnalyticsService>().firebaseObserver],
home: const SizedBox.shrink(),
home: const SplashPage(),
);
},
),
Expand Down
1 change: 1 addition & 0 deletions lib/core/di/di_setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions lib/core/di/services_module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Future<void> _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<RemoteConfigService>().baseUrl(network: Network.telos)));
_registerLazySingleton(() => IPFSManager());

/// Secure Storage
_registerLazySingleton(() => const FlutterSecureStorage());
Expand Down
17 changes: 17 additions & 0 deletions lib/core/network/ipfs/ipfs_manager.dart
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok... guess we need secret - let's load it from firebase?! At least it won't be in the code then..

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@n13 this doesnt work. it fail. can you take a look when you get some time pls.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm

return response;
}
}
10 changes: 7 additions & 3 deletions lib/core/network/models/dao_data_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> json) {
final Map<String, dynamic> 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'] ?? '',
);
}
Expand Down
5 changes: 2 additions & 3 deletions lib/core/network/networking_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 5 additions & 5 deletions lib/core/network/repository/auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class AuthRepository {
final FirebaseDatabaseService _firebaseDatabaseService;
final StreamController<AuthenticationStatus> _controller = StreamController.broadcast();

Authenticated? authenticateUser;
AuthenticationStatus currentAuthStatus = const Unknown();

AuthRepository(
this._appSharedPrefs,
Expand All @@ -42,7 +42,7 @@ class AuthRepository {
) {
status.listen((AuthenticationStatus event) {
if (event is Authenticated) {
authenticateUser = event;
currentAuthStatus = event;
}
});
}
Expand Down Expand Up @@ -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. ');
Expand Down
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -10,6 +11,7 @@ void main() async {
// Initialize Flutter
WidgetsFlutterBinding.ensureInitialized();

await dotenv.load(fileName: '.env');
await setupDependencies();

if (kDebugMode) {
Expand Down
160 changes: 115 additions & 45 deletions lib/ui/profile/components/dao_widget.dart
Original file line number Diff line number Diff line change
@@ -1,66 +1,136 @@
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 '<hash>: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,
});

@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<String>(
// 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}',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this field was called short URL or something

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<String> 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}');
// }
// }
Loading
Loading