diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 9ea6f3828..4b9e29943 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -33,6 +33,13 @@ if (keystorePropertiesFile.exists()) { android { + buildFeatures { + buildConfig = true + } + + namespace "com.friend.ios" + + // ----- BEGIN flavorDimensions (autogenerated by flutter_flavorizr) ----- flavorDimensions += "flavor-type" @@ -76,6 +83,8 @@ android { versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true + buildConfigField("String", "INTERCOM_APP_ID", "\"${System.getenv("intercom.app.id")}\"") + buildConfigField("String", "INTERCOM_ANDROID_API_KEY", "\"${System.getenv("intercom.android.api.key")}\"") } signingConfigs { @@ -105,6 +114,7 @@ android { shrinkResources true } debug{ + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' minifyEnabled true } } @@ -117,9 +127,10 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10" androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3' + implementation 'io.intercom.android:intercom-sdk:latest.release' } diff --git a/app/android/app/proguard-rules.pro b/app/android/app/proguard-rules.pro index 98d4a9122..6d36a7276 100644 --- a/app/android/app/proguard-rules.pro +++ b/app/android/app/proguard-rules.pro @@ -1,3 +1,26 @@ +# This is generated automatically by the Android Gradle plugin. +-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication +-dontwarn com.google.android.play.core.splitinstall.SplitInstallException +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest +-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState +-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener +-dontwarn com.google.android.play.core.tasks.OnFailureListener +-dontwarn com.google.android.play.core.tasks.OnSuccessListener +-dontwarn com.google.android.play.core.tasks.Task + +# XML parser stuff +-dontwarn org.xmlpull.v1.** +-dontwarn org.kxml2.io.** +-dontwarn android.content.res.** +-dontwarn org.slf4j.impl.StaticLoggerBinder + + +-keep class org.xmlpull.** { *; } +-keepclassmembers class org.xmlpull.** { *; } + #Flutter Wrapper -keep class io.flutter.app.** { *; } -keep class io.flutter.plugin.** { *; } @@ -6,6 +29,10 @@ -keep class io.flutter.** { *; } -keep class io.flutter.plugins.** { *; } +# For Awesome Notification Plugin +-keep class com.google.common.reflect.TypeToken +-keep class * extends com.google.common.reflect.TypeToken + # You might not be using firebase -keep class com.google.firebase.** { *; } -keep class com.builttoroam.devicecalendar.** { *; } diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index d450dcf00..fd36ac8f3 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -67,6 +67,7 @@ + if (project.hasProperty('android')) { + project.android { + if (namespace == null) { + namespace project.group + } + } + } + } + } } rootProject.buildDir = '../build' @@ -13,6 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } + tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index d82a0a76d..0b9e451c4 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jun 23 08:50:38 CEST 2017 +#Sun Sep 22 21:12:01 IST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip \ No newline at end of file diff --git a/app/android/settings.gradle b/app/android/settings.gradle index b12be7da6..794deab20 100644 --- a/app/android/settings.gradle +++ b/app/android/settings.gradle @@ -18,11 +18,11 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.4.2" apply false + id "com.android.application" version "8.1.4" apply false // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.3.15" apply false // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "1.8.21" apply false + id "org.jetbrains.kotlin.android" version "1.9.21" apply false } diff --git a/app/ios/Podfile b/app/ios/Podfile index a303c617b..d4a336ef1 100644 --- a/app/ios/Podfile +++ b/app/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 20ee5f46d..192c27101 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -445,7 +445,7 @@ CODE_SIGN_ENTITLEMENTS = "Runner/RunnerDebug-prod.entitlements"; DEVELOPMENT_TEAM = 9536L8KLMP; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "com.friend-app-with-wearable.ios12"; PRODUCT_NAME = Runner; TARGETED_DEVICE_FAMILY = 1; @@ -461,7 +461,7 @@ CODE_SIGN_ENTITLEMENTS = "Runner/RunnerProfile-prod.entitlements"; DEVELOPMENT_TEAM = 9536L8KLMP; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "com.friend-app-with-wearable.ios12"; PRODUCT_NAME = Runner; TARGETED_DEVICE_FAMILY = 1; @@ -477,7 +477,7 @@ CODE_SIGN_ENTITLEMENTS = "Runner/RunnerRelease-prod.entitlements"; DEVELOPMENT_TEAM = 9536L8KLMP; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "com.friend-app-with-wearable.ios12"; PRODUCT_NAME = Runner; TARGETED_DEVICE_FAMILY = 1; @@ -493,7 +493,7 @@ CODE_SIGN_ENTITLEMENTS = "Runner/RunnerDebug-dev.entitlements"; DEVELOPMENT_TEAM = 9536L8KLMP; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "com.friend-app-with-wearable.ios12.development"; PRODUCT_NAME = Runner; TARGETED_DEVICE_FAMILY = 1; @@ -509,7 +509,7 @@ CODE_SIGN_ENTITLEMENTS = "Runner/RunnerProfile-dev.entitlements"; DEVELOPMENT_TEAM = 9536L8KLMP; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "com.friend-app-with-wearable.ios12.development"; PRODUCT_NAME = Runner; TARGETED_DEVICE_FAMILY = 1; @@ -525,7 +525,7 @@ CODE_SIGN_ENTITLEMENTS = "Runner/RunnerRelease-dev.entitlements"; DEVELOPMENT_TEAM = 9536L8KLMP; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = "com.friend-app-with-wearable.ios12.development"; PRODUCT_NAME = Runner; TARGETED_DEVICE_FAMILY = 1; @@ -604,7 +604,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -904,7 +904,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -949,7 +949,7 @@ ); INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Omi; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist index 16abdd9f1..28217fb47 100644 --- a/app/ios/Runner/Info.plist +++ b/app/ios/Runner/Info.plist @@ -76,6 +76,8 @@ We need access to your photo library to allow you to upload and share photos through Instabug. PermissionGroupNotification You need to enable notifications to receive your pro-active feedback. + NSCameraUsageDescription + Camera access is required to report issues UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/app/lib/env/dev_env.dart b/app/lib/env/dev_env.dart index 7db2bff1d..2835179ec 100644 --- a/app/lib/env/dev_env.dart +++ b/app/lib/env/dev_env.dart @@ -35,4 +35,16 @@ final class DevEnv implements EnvFields { @override @EnviedField(varName: 'GLEAP_API_KEY', obfuscate: true) final String? gleapApiKey = _DevEnv.gleapApiKey; + + @override + @EnviedField(varName: 'INTERCOM_APP_ID', obfuscate: true) + final String? intercomAppId = _DevEnv.intercomAppId; + + @override + @EnviedField(varName: 'INTERCOM_IOS_API_KEY', obfuscate: true) + final String? intercomIOSApiKey = _DevEnv.intercomIOSApiKey; + + @override + @EnviedField(varName: 'INTERCOM_ANDROID_API_KEY', obfuscate: true) + final String? intercomAndroidApiKey = _DevEnv.intercomAndroidApiKey; } diff --git a/app/lib/env/env.dart b/app/lib/env/env.dart index 8a14a8046..5dfd8d84c 100644 --- a/app/lib/env/env.dart +++ b/app/lib/env/env.dart @@ -24,6 +24,12 @@ abstract class Env { static String? get googleMapsApiKey => _instance.googleMapsApiKey; static String? get gleapApiKey => _instance.gleapApiKey; + + static String? get intercomAppId => _instance.intercomAppId; + + static String? get intercomIOSApiKey => _instance.intercomIOSApiKey; + + static String? get intercomAndroidApiKey => _instance.intercomAndroidApiKey; } abstract class EnvFields { @@ -40,4 +46,10 @@ abstract class EnvFields { String? get googleMapsApiKey; String? get gleapApiKey; + + String? get intercomAppId; + + String? get intercomIOSApiKey; + + String? get intercomAndroidApiKey; } diff --git a/app/lib/env/prod_env.dart b/app/lib/env/prod_env.dart index 1debbdb07..a71587568 100644 --- a/app/lib/env/prod_env.dart +++ b/app/lib/env/prod_env.dart @@ -35,4 +35,16 @@ final class ProdEnv implements EnvFields { @override @EnviedField(varName: 'GLEAP_API_KEY', obfuscate: true) final String? gleapApiKey = _ProdEnv.gleapApiKey; + + @override + @EnviedField(varName: 'INTERCOM_APP_ID', obfuscate: true) + final String? intercomAppId = _ProdEnv.intercomAppId; + + @override + @EnviedField(varName: 'INTERCOM_IOS_API_KEY', obfuscate: true) + final String? intercomIOSApiKey = _ProdEnv.intercomIOSApiKey; + + @override + @EnviedField(varName: 'INTERCOM_ANDROID_API_KEY', obfuscate: true) + final String? intercomAndroidApiKey = _ProdEnv.intercomAndroidApiKey; } diff --git a/app/lib/main.dart b/app/lib/main.dart index 6d0f924c6..944167e1c 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -29,7 +29,6 @@ import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/providers/onboarding_provider.dart'; import 'package:friend_private/providers/plugin_provider.dart'; import 'package:friend_private/providers/speech_profile_provider.dart'; -import 'package:friend_private/providers/websocket_provider.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/services/services.dart'; import 'package:friend_private/utils/analytics/gleap.dart'; @@ -39,6 +38,7 @@ import 'package:friend_private/utils/features/calendar.dart'; import 'package:friend_private/utils/logger.dart'; import 'package:gleap_sdk/gleap_sdk.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; import 'package:opus_dart/opus_dart.dart'; import 'package:opus_flutter/opus_flutter.dart' as opus_flutter; import 'package:provider/provider.dart'; @@ -58,6 +58,13 @@ Future _init() async { await Firebase.initializeApp(options: dev.DefaultFirebaseOptions.currentPlatform, name: 'dev'); } + if (Env.intercomAppId != null) { + await Intercom.instance.initialize( + Env.intercomAppId!, + iosApiKey: Env.intercomIOSApiKey, + androidApiKey: Env.intercomAndroidApiKey, + ); + } await NotificationService.instance.initialize(); await SharedPreferencesUtil.init(); await MixpanelManager.init(); @@ -270,14 +277,20 @@ class DeciderWidget extends StatefulWidget { class _DeciderWidgetState extends State { @override void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { if (context.read().isConnected) { NotificationService.instance.saveNotificationToken(); } if (context.read().user != null) { + context.read().setupHasSpeakerProfile(); + await Intercom.instance.loginIdentifiedUser( + userId: FirebaseAuth.instance.currentUser!.uid, + ); context.read().setMessagesFromCache(); context.read().refreshMessages(); + } else { + await Intercom.instance.loginUnidentifiedUser(); } }); super.initState(); diff --git a/app/lib/pages/capture/connect.dart b/app/lib/pages/capture/connect.dart index 02b10591f..e63797579 100644 --- a/app/lib/pages/capture/connect.dart +++ b/app/lib/pages/capture/connect.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:friend_private/pages/home/device_settings.dart'; +import 'package:friend_private/pages/settings/device_settings.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/onboarding/find_device/page.dart'; import 'package:friend_private/utils/other/temp.dart'; @@ -24,9 +24,7 @@ class _ConnectDevicePageState extends State { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const DeviceSettings( - isDeviceConnected: false, - ), + builder: (context) => const DeviceSettings(), ), ); }, diff --git a/app/lib/pages/home/device.dart b/app/lib/pages/home/device.dart index 6ccbde21f..89717a074 100644 --- a/app/lib/pages/home/device.dart +++ b/app/lib/pages/home/device.dart @@ -6,10 +6,9 @@ import 'package:friend_private/services/services.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/widgets/device_widget.dart'; import 'package:gradient_borders/box_borders/gradient_box_border.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; import 'package:provider/provider.dart'; -import 'device_settings.dart'; - class ConnectedDevice extends StatefulWidget { // TODO: retrieve this from here instead of params final BTDeviceStruct? device; @@ -31,47 +30,19 @@ class _ConnectedDeviceState extends State { return await connection.disconnect(); } - Future _getDeviceInfo(String? deviceId) async { - if (deviceId == null) { - return DeviceInfo.getDeviceInfo(null, null); - } - - var connection = await ServiceManager.instance().device.ensureConnection(deviceId); - if (connection == null) { - return DeviceInfo.getDeviceInfo(null, null); - } - return await DeviceInfo.getDeviceInfo(connection.device, connection); - } - @override Widget build(BuildContext context) { var deviceName = widget.device?.name ?? SharedPreferencesUtil().deviceName; var deviceConnected = widget.device != null; return FutureBuilder( - future: _getDeviceInfo(widget.device?.id), + future: context.read().getDeviceInfo(), builder: (BuildContext context, AsyncSnapshot snapshot) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, appBar: AppBar( title: Text(deviceConnected ? 'Connected Device' : 'Paired Device'), backgroundColor: Theme.of(context).colorScheme.primary, - actions: [ - IconButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => DeviceSettings( - device: widget.device, - deviceInfo: snapshot.data, - isDeviceConnected: deviceConnected, - ), - ), - ); - }, - icon: const Icon(Icons.settings), - ) - ], ), body: Column( children: [ @@ -188,6 +159,19 @@ class _ConnectedDeviceState extends State { ), ), ), + const SizedBox(height: 8), + TextButton( + onPressed: () async { + await Intercom.instance.displayArticle('9907475-how-to-charge-the-device'); + }, + child: const Text( + 'How to charge?', + style: TextStyle( + color: Colors.white, + decoration: TextDecoration.underline, + ), + ), + ), ], ), ); diff --git a/app/lib/pages/home/device_settings.dart b/app/lib/pages/home/device_settings.dart deleted file mode 100644 index c276373f1..000000000 --- a/app/lib/pages/home/device_settings.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:friend_private/backend/preferences.dart'; -import 'package:friend_private/backend/schema/bt_device.dart'; -import 'package:friend_private/pages/home/firmware_update.dart'; -import 'package:friend_private/providers/device_provider.dart'; -import 'package:friend_private/providers/onboarding_provider.dart'; -import 'package:friend_private/services/services.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; -import 'package:gradient_borders/gradient_borders.dart'; -import 'package:provider/provider.dart'; - -import 'support.dart'; - -class DeviceSettings extends StatelessWidget { - final DeviceInfo? deviceInfo; - final BTDeviceStruct? device; - final bool isDeviceConnected; - const DeviceSettings({super.key, this.deviceInfo, this.device, this.isDeviceConnected = false}); - - // TODO: thinh, use connection directly - Future _bleDisconnectDevice(BTDeviceStruct btDevice) async { - var connection = await ServiceManager.instance().device.ensureConnection(btDevice.id); - if (connection == null) { - return Future.value(null); - } - return await connection.disconnect(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.primary, - appBar: AppBar( - title: const Text('Device Settings'), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - body: Padding( - padding: const EdgeInsets.all(4.0), - child: ListView( - children: [ - Stack( - children: [ - Column( - children: deviceSettingsWidgets(deviceInfo, device, context), - ), - if (!isDeviceConnected) - ClipRRect( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 3.0, - sigmaY: 3.0, - ), - child: Container( - height: 410, - width: double.infinity, - margin: const EdgeInsets.only(top: 10), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - spreadRadius: 5, - blurRadius: 7, - offset: const Offset(0, 3), - ), - ], - ), - child: const Center( - child: Text( - 'Connect your device to access these settings', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - )), - ), - ), - ], - ), - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SupportPage(), - ), - ); - }, - child: const ListTile( - title: Text('Guides & Support'), - trailing: Icon(Icons.arrow_forward_ios), - ), - ), - ], - ), - ), - bottomNavigationBar: isDeviceConnected - ? Padding( - padding: const EdgeInsets.only(bottom: 70, left: 30, right: 30), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), - decoration: BoxDecoration( - border: const GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 2, - ), - borderRadius: BorderRadius.circular(12), - ), - child: TextButton( - onPressed: () async { - await SharedPreferencesUtil().btDeviceStructSet(BTDeviceStruct(id: '', name: '')); - SharedPreferencesUtil().deviceName = ''; - if (device != null) { - await _bleDisconnectDevice(device!); - } - context.read().setIsConnected(false); - context.read().setConnectedDevice(null); - context.read().updateConnectingStatus(false); - context.read().stopFindDeviceTimer(); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('Your Friend is ${device == null ? "unpaired" : "disconnected"} 😔'), - )); - MixpanelManager().disconnectFriendClicked(); - }, - child: Text( - device == null ? "Unpair" : "Disconnect", - style: const TextStyle(color: Colors.white, fontSize: 16), - ), - ), - ), - ) - : const SizedBox(), - ); - } -} - -List deviceSettingsWidgets(DeviceInfo? deviceInfo, BTDeviceStruct? device, BuildContext context) { - return [ - ListTile( - title: const Text('Device Name'), - subtitle: Text(device?.name ?? 'Friend'), - ), - ListTile( - title: const Text('Device ID'), - subtitle: Text(device?.id ?? '12AB34CD:56EF78GH'), - ), - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FirmwareUpdate( - deviceInfo: deviceInfo!, - device: device, - ), - ), - ); - }, - child: ListTile( - title: const Text('Firmware Update'), - subtitle: Text(deviceInfo?.firmwareRevision ?? '1.0.2'), - trailing: const Icon(Icons.arrow_forward_ios), - ), - ), - ListTile( - title: const Text('Hardware Revision'), - subtitle: Text(deviceInfo?.hardwareRevision ?? 'XIAO'), - ), - ListTile( - title: const Text('Model Number'), - subtitle: Text(deviceInfo?.modelNumber ?? 'Friend'), - ), - ListTile( - title: const Text('Manufacturer Name'), - subtitle: Text(deviceInfo?.manufacturerName ?? 'Based Hardware'), - ), - ]; -} diff --git a/app/lib/pages/home/support.dart b/app/lib/pages/home/support.dart index 60a9cec79..f5b74125a 100644 --- a/app/lib/pages/home/support.dart +++ b/app/lib/pages/home/support.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:friend_private/widgets/dialog.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../settings/widgets/expansion_tile_card.dart'; + class SupportPage extends StatelessWidget { const SupportPage({super.key}); @@ -17,63 +18,139 @@ class SupportPage extends StatelessWidget { padding: const EdgeInsets.all(4.0), child: ListView( children: [ - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (c) => getDialog( - context, - () => Navigator.pop(context), - () {}, - singleButton: true, - 'Device Not Appearing After Update', - 'If your device doesn\'t appear in the list after updating the firmware, try restarting it. If it keeps blinking after restarting and doesn\'t appear in list, then the update was not successful and got corrupted.\n\nYou can try updating the firmware manually by following the guide mentioned on this page.', - okButtonText: 'Ok, I understand', + ExpansionTileCard( + title: 'How to Update Firmware', + baseColor: Theme.of(context).colorScheme.primary, + expandedColor: const Color.fromARGB(255, 29, 29, 29), + elevation: 0, + expandedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + collapsedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + children: const [ + Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'To update the firmware, follow the steps below:', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + SizedBox(height: 8), + Text( + '1. Make sure your device is connected to the app.', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + SizedBox(height: 8), + Text( + '2. Go to the Device Settings page.', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + SizedBox(height: 8), + Text( + '3. Tap on the "Update Firmware" button.', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + SizedBox(height: 8), + Text( + '4. Wait for the update to complete.', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + ], ), - ); - }, - child: const ListTile( - title: Text('My Device doesn\'t appear in the list or it keeps blinking after updating the firmware'), - trailing: Icon(Icons.arrow_forward_ios), - ), + ), + ], ), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (c) => getDialog( - context, - () => Navigator.pop(context), - () {}, - singleButton: true, - 'Firmware Update on v1.0.2', - 'The app only supports updating the firmware from v1.0.3 onwards. If you are on v1.0.2 or below, you will need to update the firmware manually to v1.0.3 and bootloader to v0.9.0.\n\nYou can find the guide on how to update the firmware manually on this page.', - okButtonText: 'Ok, I understand', + ExpansionTileCard( + title: 'My Device doesn\'t appear in the list after firmware update', + baseColor: Theme.of(context).colorScheme.primary, + expandedColor: const Color.fromARGB(255, 29, 29, 29), + elevation: 0, + expandedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + collapsedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + children: const [ + Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'If your device doesn\'t appear in the list after updating the firmware, try restarting it. If it keeps blinking after restarting and doesn\'t appear in list, then the update was not successful and got corrupted.\n', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + Text( + 'You can try updating the firmware manually by following the guide mentioned on this page.', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + ], ), - ); - }, - child: const ListTile( - title: Text('I am on v1.0.2 and can\'t update the firmware through the app'), - trailing: Icon(Icons.arrow_forward_ios), - ), + ), + ], ), - GestureDetector( - onTap: () { - launchUrl(Uri.parse('https://github.com/BasedHardware/Omi/releases/tag/v1.0.3-firmware')); - }, - child: const ListTile( - title: Text('How to Update Bootloader'), - trailing: Icon(Icons.arrow_forward_ios), - ), + ExpansionTileCard( + title: 'I am on v1.0.2 and can\'t update the firmware through the app', + baseColor: Theme.of(context).colorScheme.primary, + expandedColor: const Color.fromARGB(255, 29, 29, 29), + elevation: 0, + expandedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + collapsedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + children: const [ + Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'The app only supports updating the firmware from v1.0.3 onwards. If you are on v1.0.2 or below, you will need to update the firmware manually to v1.0.3 and bootloader to v0.9.0.\n', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + Text( + 'You can find the guide on how to update the firmware manually on this page.', + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255)), + ), + ], + ), + ), + ], ), - GestureDetector( - onTap: () { - launchUrl(Uri.parse('https://github.com/BasedHardware/Omi/releases/tag/v1.0.3-firmware')); - }, - child: const ListTile( - title: Text('How to Update Firmware Manually'), - trailing: Icon(Icons.arrow_forward_ios), - ), + ExpansionTileCard( + title: 'How to Update Bootloader', + baseColor: Theme.of(context).colorScheme.primary, + expandedColor: const Color.fromARGB(255, 29, 29, 29), + elevation: 0, + expandedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + collapsedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + children: [ + InkWell( + onTap: () { + launchUrl(Uri.parse('https://github.com/BasedHardware/Omi/releases/tag/v1.0.3-firmware')); + }, + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text("Click here to view the guide", + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255))), + ), + ) + ], + ), + ExpansionTileCard( + title: 'How to Update Firmware Manually', + baseColor: Theme.of(context).colorScheme.primary, + expandedColor: const Color.fromARGB(255, 29, 29, 29), + elevation: 0, + expandedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + collapsedTextStyle: TextStyle(color: Theme.of(context).textTheme.titleMedium!.color), + children: [ + InkWell( + onTap: () { + launchUrl(Uri.parse('https://github.com/BasedHardware/Omi/releases/tag/v1.0.3-firmware')); + }, + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text("Click here to view the guide", + style: TextStyle(color: Color.fromARGB(255, 255, 255, 255))), + ), + ) + ], ), ], ), diff --git a/app/lib/pages/settings/device_settings.dart b/app/lib/pages/settings/device_settings.dart new file mode 100644 index 000000000..6f26aecab --- /dev/null +++ b/app/lib/pages/settings/device_settings.dart @@ -0,0 +1,196 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/backend/schema/bt_device.dart'; +import 'package:friend_private/pages/home/firmware_update.dart'; +import 'package:friend_private/providers/device_provider.dart'; +import 'package:friend_private/providers/onboarding_provider.dart'; +import 'package:friend_private/services/services.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:gradient_borders/gradient_borders.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; +import 'package:provider/provider.dart'; + +class DeviceSettings extends StatefulWidget { + const DeviceSettings({super.key}); + + @override + State createState() => _DeviceSettingsState(); +} + +class _DeviceSettingsState extends State { + // TODO: thinh, use connection directly + Future _bleDisconnectDevice(BTDeviceStruct btDevice) async { + var connection = await ServiceManager.instance().device.ensureConnection(btDevice.id); + if (connection == null) { + return Future.value(null); + } + return await connection.disconnect(); + } + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().getDeviceInfo(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, provider, child) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: AppBar( + title: const Text('Device Settings'), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + body: Padding( + padding: const EdgeInsets.all(4.0), + child: ListView( + children: [ + Stack( + children: [ + Column( + children: deviceSettingsWidgets(provider.deviceInfo, provider.connectedDevice, context), + ), + if (!provider.isConnected) + ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 3.0, + sigmaY: 3.0, + ), + child: Container( + height: 410, + width: double.infinity, + margin: const EdgeInsets.only(top: 10), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: const Center( + child: Text( + 'Connect your device to access these settings', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ], + ), + GestureDetector( + onTap: () async { + await Intercom.instance.displayArticle('9907475-how-to-charge-the-device'); + }, + child: const ListTile( + title: Text('How to charge device'), + trailing: Icon(Icons.arrow_forward_ios), + ), + ), + ], + ), + ), + bottomNavigationBar: provider.isConnected + ? Padding( + padding: const EdgeInsets.only(bottom: 70, left: 30, right: 30), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), + decoration: BoxDecoration( + border: const GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ), + child: TextButton( + onPressed: () async { + await SharedPreferencesUtil().btDeviceStructSet(BTDeviceStruct(id: '', name: '')); + SharedPreferencesUtil().deviceName = ''; + if (provider.connectedDevice != null) { + await _bleDisconnectDevice(provider.connectedDevice!); + } + provider.setIsConnected(false); + provider.setConnectedDevice(null); + provider.updateConnectingStatus(false); + context.read().stopFindDeviceTimer(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + 'Your Friend is ${provider.connectedDevice == null ? "unpaired" : "disconnected"} 😔'), + )); + MixpanelManager().disconnectFriendClicked(); + }, + child: Text( + provider.connectedDevice == null ? "Unpair" : "Disconnect", + style: const TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ), + ) + : const SizedBox(), + ); + }); + } +} + +List deviceSettingsWidgets(DeviceInfo? deviceInfo, BTDeviceStruct? device, BuildContext context) { + return [ + ListTile( + title: const Text('Device Name'), + subtitle: Text(device?.name ?? 'Friend'), + ), + ListTile( + title: const Text('Device ID'), + subtitle: Text(device?.id ?? '12AB34CD:56EF78GH'), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FirmwareUpdate( + deviceInfo: deviceInfo!, + device: device, + ), + ), + ); + }, + child: ListTile( + title: const Text('Firmware Update'), + subtitle: Text(deviceInfo?.firmwareRevision ?? '1.0.2'), + trailing: const Icon(Icons.arrow_forward_ios), + ), + ), + ListTile( + title: const Text('Hardware Revision'), + subtitle: Text(deviceInfo?.hardwareRevision ?? 'XIAO'), + ), + ListTile( + title: const Text('Model Number'), + subtitle: Text(deviceInfo?.modelNumber ?? 'Friend'), + ), + ListTile( + title: const Text('Manufacturer Name'), + subtitle: Text(deviceInfo?.manufacturerName ?? 'Based Hardware'), + ), + ]; +} diff --git a/app/lib/pages/settings/page.dart b/app/lib/pages/settings/page.dart index 250986fc5..e274742fd 100644 --- a/app/lib/pages/settings/page.dart +++ b/app/lib/pages/settings/page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/auth.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/main.dart'; +import 'package:friend_private/pages/home/support.dart'; import 'package:friend_private/pages/plugins/page.dart'; import 'package:friend_private/pages/settings/about.dart'; import 'package:friend_private/pages/settings/calendar.dart'; @@ -11,8 +12,11 @@ import 'package:friend_private/pages/settings/widgets.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/widgets/dialog.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'device_settings.dart'; + class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -89,12 +93,44 @@ class _SettingsPageState extends State { SharedPreferencesUtil().recordingsLanguage = _selectedLanguage; MixpanelManager().recordingLanguageChanged(_selectedLanguage); }, _selectedLanguage), + getItemAddOn2( + 'Need Help? Chat with us', + () async { + await Intercom.instance.displayMessenger(); + }, + icon: Icons.chat, + ), + const SizedBox(height: 20), getItemAddOn2( 'Profile', () => routeToPage(context, const ProfilePage()), icon: Icons.person, ), const SizedBox(height: 20), + getItemAddOn2( + 'Device Settings', + () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DeviceSettings(), + ), + ); + }, + icon: Icons.bluetooth_connected_sharp, + ), + const SizedBox(height: 8), + getItemAddOn2( + 'Guides & Tutorials', + () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const SupportPage(), + ), + ); + }, + icon: Icons.help_outline_outlined, + ), + const SizedBox(height: 20), getItemAddOn2( 'Plugins', () => routeToPage(context, const PluginsPage()), diff --git a/app/lib/pages/settings/widgets/expansion_tile_card.dart b/app/lib/pages/settings/widgets/expansion_tile_card.dart new file mode 100644 index 000000000..0ad2bf317 --- /dev/null +++ b/app/lib/pages/settings/widgets/expansion_tile_card.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; + +class ExpansionTileCard extends StatefulWidget { + const ExpansionTileCard({ + super.key, + this.leading, + required this.title, + this.subtitle, + this.onExpansionChanged, + this.children = const [], + this.trailing, + this.borderRadius = const BorderRadius.all(Radius.circular(8.0)), + this.elevation = 2.0, + this.initialElevation = 0.0, + this.initiallyExpanded = false, + this.initialPadding = EdgeInsets.zero, + this.finalPadding = const EdgeInsets.only(bottom: 6.0), + this.contentPadding, + this.baseColor, + this.expandedColor, + this.expandedTextColor, + this.duration = const Duration(milliseconds: 200), + this.elevationCurve = Curves.easeOut, + this.heightFactorCurve = Curves.easeIn, + this.turnsCurve = Curves.easeIn, + this.colorCurve = Curves.easeIn, + this.paddingCurve = Curves.easeIn, + this.isThreeLine = false, + this.shadowColor = const Color(0xffaaaaaa), + this.animateTrailing = false, + required this.expandedTextStyle, + required this.collapsedTextStyle, + }); + + final bool isThreeLine; + final Widget? leading; + final String title; + final Widget? subtitle; + final ValueChanged? onExpansionChanged; + final List children; + final Widget? trailing; + final bool animateTrailing; + final BorderRadiusGeometry borderRadius; + final double elevation; + final double initialElevation; + final Color shadowColor; + final bool initiallyExpanded; + final EdgeInsetsGeometry initialPadding; + final EdgeInsetsGeometry finalPadding; + final EdgeInsetsGeometry? contentPadding; + final Color? baseColor; + + final TextStyle expandedTextStyle; + + final TextStyle collapsedTextStyle; + final Color? expandedColor; + final Color? expandedTextColor; + final Duration duration; + final Curve elevationCurve; + final Curve heightFactorCurve; + final Curve turnsCurve; + final Curve colorCurve; + final Curve paddingCurve; + + @override + ExpansionTileCardState createState() => ExpansionTileCardState(); +} + +class ExpansionTileCardState extends State with SingleTickerProviderStateMixin { + static final Animatable _halfTween = Tween(begin: 0.0, end: 0.5); + + final ColorTween _headerColorTween = ColorTween(); + final ColorTween _iconColorTween = ColorTween(); + final ColorTween _materialColorTween = ColorTween(); + late EdgeInsetsTween _edgeInsetsTween; + late Animatable _elevationTween; + late Animatable _heightFactorTween; + late Animatable _turnsTween; + late Animatable _colorTween; + late Animatable _paddingTween; + + late AnimationController _controller; + late Animation _iconTurns; + late Animation _heightFactor; + late Animation _elevation; + late Animation _headerColor; + late Animation _iconColor; + late Animation _materialColor; + late Animation _padding; + + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + _edgeInsetsTween = EdgeInsetsTween( + begin: widget.initialPadding as EdgeInsets?, + end: widget.finalPadding as EdgeInsets?, + ); + _elevationTween = CurveTween(curve: widget.elevationCurve); + _heightFactorTween = CurveTween(curve: widget.heightFactorCurve); + _colorTween = CurveTween(curve: widget.colorCurve); + _turnsTween = CurveTween(curve: widget.turnsCurve); + _paddingTween = CurveTween(curve: widget.paddingCurve); + + _controller = AnimationController(duration: widget.duration, vsync: this); + _heightFactor = _controller.drive(_heightFactorTween); + _iconTurns = _controller.drive(_halfTween.chain(_turnsTween)); + _headerColor = _controller.drive(_headerColorTween.chain(_colorTween)); + _materialColor = _controller.drive(_materialColorTween.chain(_colorTween)); + _iconColor = _controller.drive(_iconColorTween.chain(_colorTween)); + _elevation = + _controller.drive(Tween(begin: widget.initialElevation, end: widget.elevation).chain(_elevationTween)); + _padding = _controller.drive(_edgeInsetsTween.chain(_paddingTween)); + _isExpanded = PageStorage.of(context).readState(context) as bool? ?? widget.initiallyExpanded; + if (_isExpanded) _controller.value = 1.0; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + // Credit: Simon Lightfoot - https://stackoverflow.com/a/48935106/955974 + void _setExpansion(bool shouldBeExpanded) { + if (shouldBeExpanded != _isExpanded) { + setState(() { + _isExpanded = shouldBeExpanded; + if (_isExpanded) { + _controller.forward(); + } else { + _controller.reverse().then((void value) { + if (!mounted) return; + setState(() {}); + }); + } + PageStorage.of(context).writeState(context, _isExpanded); + }); + if (widget.onExpansionChanged != null) { + widget.onExpansionChanged!(_isExpanded); + } + } + } + + void expand() { + _setExpansion(true); + } + + void collapse() { + _setExpansion(false); + } + + void toggleExpansion() { + _setExpansion(!_isExpanded); + } + + Widget _buildChildren(BuildContext context, Widget? child) { + return Padding( + padding: _padding.value, + child: Material( + type: MaterialType.card, + color: _materialColor.value, + borderRadius: widget.borderRadius, + elevation: _elevation.value, + shadowColor: widget.shadowColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + customBorder: RoundedRectangleBorder(borderRadius: widget.borderRadius), + onTap: toggleExpansion, + child: ListTileTheme.merge( + iconColor: _iconColor.value, + textColor: _headerColor.value, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: ListTile( + isThreeLine: widget.isThreeLine, + contentPadding: widget.contentPadding, + leading: widget.leading, + title: Text( + widget.title, + style: _isExpanded ? widget.expandedTextStyle : widget.collapsedTextStyle, + ), + subtitle: widget.subtitle, + trailing: RotationTransition( + turns: widget.trailing == null || widget.animateTrailing + ? _iconTurns + : const AlwaysStoppedAnimation(0), + child: widget.trailing ?? + Icon( + Icons.expand_more, + color: _isExpanded ? widget.expandedTextStyle.color : widget.collapsedTextStyle.color, + ), + ), + ), + ), + ), + ), + ClipRect( + child: Align( + heightFactor: _heightFactor.value, + child: child, + ), + ), + ], + ), + ), + ); + } + + @override + void didChangeDependencies() { + final ThemeData theme = Theme.of(context); + _headerColorTween + ..begin = theme.textTheme.titleMedium!.color + ..end = widget.expandedTextColor ?? theme.colorScheme.secondary; + _iconColorTween + ..begin = theme.unselectedWidgetColor + ..end = widget.expandedTextColor ?? theme.colorScheme.secondary; + _materialColorTween + ..begin = widget.baseColor ?? theme.canvasColor + ..end = widget.expandedColor ?? theme.cardColor; + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + final bool closed = !_isExpanded && _controller.isDismissed; + return AnimatedBuilder( + animation: _controller.view, + builder: _buildChildren, + child: closed ? null : Column(children: widget.children), + ); + } +} diff --git a/app/lib/providers/device_provider.dart b/app/lib/providers/device_provider.dart index ce064d659..fef982dcd 100644 --- a/app/lib/providers/device_provider.dart +++ b/app/lib/providers/device_provider.dart @@ -16,6 +16,7 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption bool isConnecting = false; bool isConnected = false; BTDeviceStruct? connectedDevice; + DeviceInfo? deviceInfo; StreamSubscription>? _bleBatteryLevelListener; int batteryLevel = -1; Timer? _reconnectionTimer; @@ -38,6 +39,19 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption notifyListeners(); } + Future getDeviceInfo() async { + if (connectedDevice == null) { + return DeviceInfo.getDeviceInfo(null, null); + } + var connection = await ServiceManager.instance().device.ensureConnection(connectedDevice!.id); + if (connection == null) { + return DeviceInfo.getDeviceInfo(null, null); + } + deviceInfo = await DeviceInfo.getDeviceInfo(connection.device, connection); + notifyListeners(); + return deviceInfo!; + } + // TODO: thinh, use connection directly Future>?> _getBleBatteryLevelListener( String deviceId, { diff --git a/app/lib/services/notification_service.dart b/app/lib/services/notification_service.dart index a3866c145..3a1b7ff39 100644 --- a/app/lib/services/notification_service.dart +++ b/app/lib/services/notification_service.dart @@ -15,6 +15,7 @@ import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/message.dart'; import 'package:friend_private/main.dart'; import 'package:friend_private/pages/home/page.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; class NotificationService { NotificationService._(); @@ -124,6 +125,7 @@ class NotificationService { if (token == null) return; String timeZone = await getTimeZone(); if (FirebaseAuth.instance.currentUser != null && token.isNotEmpty) { + await Intercom.instance.sendTokenToIntercom(token); await saveFcmTokenServer(token: token, timeZone: timeZone); } } diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 4c568deb1..cfde5d5eb 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: awesome_notifications_core: ^0.9.3 awesome_notifications: any mime: ^1.0.5 - instabug_flutter: ^13.0.0 + instabug_flutter: ^13.3.0 instabug_http_client: ^2.4.0 mixpanel_flutter: ^2.2.0 expandable_text: ^2.3.0 @@ -94,11 +94,17 @@ dependencies: web_socket_channel: ^3.0.1 talker_flutter: + intercom_flutter: + dependency_overrides: http: ^1.2.1 uuid: ^4.4.0 js: ^0.7.1 + opus_flutter_android: + git: + url: https://github.com/mdmohsin7/opus_flutter.git + path: opus_flutter_android # flutter_blue_plus: # git: # url: https://github.com/chipweinberger/flutter_blue_plus