diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index cba99563df..f6b66bb108 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2684,6 +2684,22 @@ "privacyTextAnd": " and ", "privacyTextTerms": "terms of service", "createGroupLimitationWarning": "Only Tawkie / Matrix users can be added to a group chat.", + "joinBetaTitle": "Join the Beta", + "betaJoinExplanation": "## How to test the Beta version and access new features in advance?", + "betaJoinBenefit": "Participating in the Beta allows you to access application updates in advance and be the first to test new features!", + "iosInstructionsTitle": "On iOS:", + "installTestflight": "- Install Apple Testflight", + "downloadTestflightButton": "Download Apple Testflight", + "downloadBetaIOSButton": "Download the iOS Beta", + "joinBetaGroup": "- Join the Tawkie Beta group: #beta", + "androidInstructionsTitle": "On Android:", + "joinBetaPlayStore": "- Join the Beta from the Play Store or online", + "downloadBetaAndroidButton": "Download the Android Beta", + "joinBetaButtonLabel": "Join the Beta group", + "joinBetaError": "Something went wrong: ", + "failedToJoinRoom": "Failed to join room: ", + "roomIdNullError": "Room ID is null for alias: ", + "roomNotFoundError": "Room not found after joining", "seeBotsRoom":"See bots", "leaveTheConvDesc": "Do you really want to leave this conversation?", "leaveTheConvSuccess": "You have left the conversation.", diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index f7e9d76843..8a898ddf68 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -2500,6 +2500,22 @@ "privacyTextAnd": " et ", "privacyTextTerms": "conditions générales d’utilisation et de services", "createGroupLimitationWarning": "Seul.es les utilisateur.ices Tawkie peuvent être rajouté.es aux groupes.", + "joinBetaTitle": "Rejoindre la Beta", + "betaJoinExplanation": "## Comment tester la version Beta et accéder aux nouvelles fonctionnalités en avance ?", + "betaJoinBenefit": "Participer à la Beta permet d'avoir accès aux mises à jour de l'application en avance et tester en premier.ère les nouvelles fonctionnalités !", + "iosInstructionsTitle": "Sous iOS :", + "installTestflight": "- Installer Apple Testflight", + "downloadTestflightButton": "Télécharger Apple Testflight", + "downloadBetaIOSButton": "Télécharger la Beta iOS", + "joinBetaGroup": "- Rejoindre le groupe Tawkie de la Beta : #beta", + "androidInstructionsTitle": "Sous Android :", + "joinBetaPlayStore": "- Rejoindre la Beta depuis le Play Store ou en ligne", + "downloadBetaAndroidButton": "Télécharger la Beta Android", + "joinBetaButtonLabel": "Rejoindre le groupe Beta", + "joinBetaError": "Une erreur s'est produite : ", + "failedToJoinRoom": "Échec de la connexion à la salle : ", + "roomIdNullError": "L'ID de la salle est nul pour l'alias : ", + "roomNotFoundError": "Salle introuvable après la connexion", "seeBotsRoom":"Voir les bots", "leaveTheConvDesc": "Voulez-vous vraiment quitter cette conversation ?", "leaveTheConvSuccess": "Vous avez quitté la conversation.", diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 8b7ed48ff9..42111cefba 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -75,6 +75,23 @@ abstract class AppConfig { static const String baseUrl = kDebugMode ? stagingUrl : productionUrl; static String tawkieSubscriptionIdentifier = 'Tawkie subscription'; + // URLs for Beta Join + static const String testflightAppUrl = 'https://apps.apple.com/us/app/testflight/id899247664'; + static const String appleBetaUrl = 'https://testflight.apple.com/join/daXe0NfW'; + static const String playStoreUrl = 'https://play.google.com/store/apps/details?id=fr.tawkie.app'; + static const String androidBetaUrl = 'https://play.google.com/apps/testing/fr.tawkie.app'; + + static const String iosUrl = 'itms-apps://itunes.apple.com/app/id899247664'; + static const String androidUrl = 'market://details?id=fr.tawkie.app'; + + static const String prodBetaAlias = 'beta'; + static const String testBetaAlias = 'testbeta'; + static const String betaAlias = kDebugMode ? testBetaAlias : prodBetaAlias; + static const String serverStagingUrl = ':staging.tawkie.fr'; + static const String serverProductionUrl = ':alpha.tawkie.fr'; + static const String server = kDebugMode ? serverStagingUrl : serverProductionUrl; + static const String roomAlias = '#$betaAlias$server'; + static void loadFromJson(Map json) { if (json['chat_color'] != null) { try { diff --git a/lib/config/routes.dart b/lib/config/routes.dart index abb3dcd54a..c7b1e61edf 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -2,14 +2,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - import 'package:go_router/go_router.dart'; - import 'package:tawkie/config/themes.dart'; import 'package:tawkie/pages/add_bridge/add_bridge.dart'; -import 'package:tawkie/pages/add_bridge/add_bridge_body.dart'; import 'package:tawkie/pages/archive/archive.dart'; import 'package:tawkie/pages/auth/auth.dart'; +import 'package:tawkie/pages/beta/beta.dart'; import 'package:tawkie/pages/chat/chat.dart'; import 'package:tawkie/pages/chat_access_settings/chat_access_settings_controller.dart'; import 'package:tawkie/pages/chat_details/chat_details.dart'; @@ -49,7 +47,8 @@ abstract class AppRoutes { final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); final sessionToken = await _secureStorage.read(key: 'sessionToken'); - final bool isLoggedKratos = sessionToken is String && sessionToken.isNotEmpty; + final bool isLoggedKratos = + sessionToken is String && sessionToken.isNotEmpty; final bool isLoggedMatrix = Matrix.of(context).client.isLogged(); final bool preAuth = state.fullPath!.startsWith('/home'); @@ -306,6 +305,15 @@ abstract class AppRoutes { ), ], ), + GoRoute( + path: 'joinBeta', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + const BetaJoinPage(), + ), + redirect: loggedOutRedirect, + ), // Route to social networking page via chat bot // The entire path is: /rooms/settings/addbridgebot GoRoute( diff --git a/lib/pages/beta/android_instructions.dart b/lib/pages/beta/android_instructions.dart new file mode 100644 index 0000000000..b2e6b2f792 --- /dev/null +++ b/lib/pages/beta/android_instructions.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:tawkie/config/app_config.dart'; +import 'package:tawkie/pages/beta/instructions.dart'; + +class AndroidInstructions extends BetaInstructions { + const AndroidInstructions({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.of(context)!.androidInstructionsTitle, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Text( + L10n.of(context)!.joinBetaPlayStore, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 10.0), + ElevatedButton( + onPressed: () async { + final bool success = await openUrl(AppConfig.playStoreUrl, context); + if (!success) { + await openUrl(AppConfig.androidBetaUrl, context); + } + }, + child: Text(L10n.of(context)!.downloadBetaAndroidButton), + ), + ], + ); + } +} diff --git a/lib/pages/beta/beta.dart b/lib/pages/beta/beta.dart new file mode 100644 index 0000000000..5383da4317 --- /dev/null +++ b/lib/pages/beta/beta.dart @@ -0,0 +1,145 @@ +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; +import 'package:tawkie/config/app_config.dart'; +import 'package:tawkie/widgets/matrix.dart'; + +import 'android_instructions.dart'; +import 'ios_instructions.dart'; + +class BetaJoinPage extends StatelessWidget { + const BetaJoinPage({super.key}); + + Future joinGroup({ + required BuildContext context, + }) async { + final client = Matrix.of(context).client; + const String roomAlias = AppConfig.roomAlias; + + try { + if (kDebugMode) { + print('Attempting to join room: $roomAlias'); + } + + // Get room ID from alias + final roomAliasResult = await client.getRoomIdByAlias(roomAlias); + final roomId = roomAliasResult.roomId; + + if (roomId == null) { + final errorMsg = L10n.of(context)!.failedToJoinRoom + + L10n.of(context)!.roomIdNullError + + roomAlias; + if (kDebugMode) { + print(errorMsg); + } + throw Exception(errorMsg); + } + + // Check if the user is already a member of the room + final room = client.getRoomById(roomId); + if (room != null && room.membership == Membership.join) { + // Navigate directly to the room + context.go('/rooms/$roomId'); + return; + } + + final result = await showFutureLoadingDialog( + context: context, + future: () async { + try { + final waitForRoom = client.waitForRoomInSync(roomId, join: true); + + await client.joinRoom(roomId); + await waitForRoom; + + return roomId; + } catch (e) { + final errorMsg = L10n.of(context)!.failedToJoinRoom + e.toString(); + if (kDebugMode) { + print(errorMsg); + } + throw Exception(errorMsg); + } + }, + ); + + if (result.error == null) { + Navigator.of(context).pop(); + + final joinedRoom = client.getRoomById(result.result!); + if (joinedRoom == null) { + final errorMsg = L10n.of(context)!.failedToJoinRoom + + L10n.of(context)!.roomNotFoundError; + if (kDebugMode) { + print(errorMsg); + } + throw Exception(errorMsg); + } + + // Navigate to the room + context.go('/rooms/${result.result!}'); + } else { + final errorMsg = + L10n.of(context)!.failedToJoinRoom + result.error.toString(); + if (kDebugMode) { + print(errorMsg); + } + final snackBar = SnackBar(content: Text(errorMsg)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + } catch (e) { + final errorMsg = L10n.of(context)!.joinBetaError + e.toString(); + if (kDebugMode) { + print(errorMsg); + } + final snackBar = SnackBar(content: Text(errorMsg)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context)!.joinBetaTitle), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + Text( + L10n.of(context)!.betaJoinExplanation, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20.0), + Text( + L10n.of(context)!.betaJoinBenefit, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20.0), + if (Platform.isIOS) const IOSInstructions(), + if (Platform.isAndroid) const AndroidInstructions(), + const Divider(thickness: 1), + Text( + L10n.of(context)!.joinBetaGroup, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 10.0), + ElevatedButton.icon( + onPressed: () async { + await joinGroup(context: context); + }, + icon: const Icon(Icons.new_releases), + label: Text(L10n.of(context)!.joinBetaButtonLabel), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/beta/instructions.dart b/lib/pages/beta/instructions.dart new file mode 100644 index 0000000000..e764543c71 --- /dev/null +++ b/lib/pages/beta/instructions.dart @@ -0,0 +1,25 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:url_launcher/url_launcher.dart'; + +abstract class BetaInstructions extends StatelessWidget { + const BetaInstructions({super.key}); + + // Launch url in browser or device app + Future openUrl(String url, BuildContext context) async { + try { + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + return true; + } + } catch (e) { + if (kDebugMode) { + print('Error to lauch url: $e'); + } + final snackBar = SnackBar(content: Text(L10n.of(context)!.tryAgain)); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + return false; + } +} diff --git a/lib/pages/beta/ios_instructions.dart b/lib/pages/beta/ios_instructions.dart new file mode 100644 index 0000000000..49c549e342 --- /dev/null +++ b/lib/pages/beta/ios_instructions.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:tawkie/config/app_config.dart'; +import 'package:tawkie/pages/beta/instructions.dart'; + +class IOSInstructions extends BetaInstructions { + const IOSInstructions({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.of(context)!.iosInstructionsTitle, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Text( + L10n.of(context)!.installTestflight, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 10.0), + ElevatedButton( + onPressed: () async { + await openUrl(AppConfig.testflightAppUrl, context); + }, + child: Text(L10n.of(context)!.downloadTestflightButton), + ), + const Divider(thickness: 1), + Text( + L10n.of(context)!.joinBetaTitle, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 10.0), + ElevatedButton( + onPressed: () async { + await openUrl(AppConfig.appleBetaUrl, context); + }, + child: Text(L10n.of(context)!.downloadBetaIOSButton), + ), + ], + ); + } +} diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 0c2f16f59a..b0d2d13d67 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,15 +1,15 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; import 'package:tawkie/utils/fluffy_share.dart'; - +import 'package:tawkie/utils/platform_infos.dart'; import 'package:tawkie/widgets/avatar.dart'; import 'package:tawkie/widgets/matrix.dart'; + import 'chat_list.dart'; class ClientChooserButton extends StatelessWidget { @@ -80,6 +80,18 @@ class ClientChooserButton extends StatelessWidget { ], ), ), + // Check if the device is mobile + if (PlatformInfos.isMobile) + PopupMenuItem( + value: SettingsAction.joinBeta, + child: Row( + children: [ + const Icon(Icons.new_releases), + const SizedBox(width: 18), + Text(L10n.of(context)!.joinBetaTitle), + ], + ), + ), PopupMenuItem( value: SettingsAction.settings, child: Row( @@ -288,6 +300,9 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.archive: context.go('/rooms/archive'); break; + case SettingsAction.joinBeta: + context.go('/rooms/settings/joinBeta'); + break; // Redirect to bot social network connection page case SettingsAction.addBridgeBot: context.go('/rooms/settings/addbridgebot'); @@ -377,4 +392,5 @@ enum SettingsAction { invite, settings, archive, + joinBeta, }