diff --git a/app/lib/main.dart b/app/lib/main.dart index d11cbed60..2ba2417e3 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -32,11 +32,11 @@ import 'package:friend_private/providers/speech_profile_provider.dart'; import 'package:friend_private/services/notifications.dart'; import 'package:friend_private/services/services.dart'; import 'package:friend_private/utils/analytics/growthbook.dart'; +import 'package:friend_private/utils/analytics/intercom.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/features/calendar.dart'; import 'package:friend_private/utils/logger.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'; @@ -56,13 +56,7 @@ 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 IntercomManager().initIntercom(); await NotificationService.instance.initialize(); await SharedPreferencesUtil.init(); await MixpanelManager.init(); @@ -281,15 +275,16 @@ class _DeciderWidgetState extends State { if (context.read().user != null) { context.read().setupHasSpeakerProfile(); - await Intercom.instance.loginIdentifiedUser( + await IntercomManager.instance.intercom.loginIdentifiedUser( userId: FirebaseAuth.instance.currentUser!.uid, ); context.read().setMessagesFromCache(); context.read().setPluginsFromCache(); context.read().refreshMessages(); } else { - await Intercom.instance.loginUnidentifiedUser(); + await IntercomManager.instance.intercom.loginUnidentifiedUser(); } + IntercomManager.instance.setUserAttributes(); }); super.initState(); } diff --git a/app/lib/pages/home/device.dart b/app/lib/pages/home/device.dart index 7fb84bb08..c0125dc21 100644 --- a/app/lib/pages/home/device.dart +++ b/app/lib/pages/home/device.dart @@ -3,10 +3,10 @@ import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/providers/device_provider.dart'; import 'package:friend_private/services/services.dart'; +import 'package:friend_private/utils/analytics/intercom.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'; class ConnectedDevice extends StatefulWidget { @@ -162,7 +162,7 @@ class _ConnectedDeviceState extends State { const SizedBox(height: 8), TextButton( onPressed: () async { - await Intercom.instance.displayArticle('9907475-how-to-charge-the-device'); + await IntercomManager.instance.displayChargingArticle(); }, child: const Text( 'Issues charging?', diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 19c017ee9..19ce57cfd 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -22,6 +22,7 @@ import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/providers/plugin_provider.dart'; import 'package:friend_private/services/notifications.dart'; +import 'package:friend_private/utils/analytics/analytics_manager.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/audio/foreground.dart'; import 'package:friend_private/utils/other/temp.dart'; @@ -45,11 +46,11 @@ class _HomePageWrapperState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { if (SharedPreferencesUtil().notificationsEnabled != await Permission.notification.isGranted) { SharedPreferencesUtil().notificationsEnabled = await Permission.notification.isGranted; - MixpanelManager().setUserProperty('Notifications Enabled', SharedPreferencesUtil().notificationsEnabled); + AnalyticsManager().setUserAttribute('Notifications Enabled', SharedPreferencesUtil().notificationsEnabled); } if (SharedPreferencesUtil().locationEnabled != await Permission.location.isGranted) { SharedPreferencesUtil().locationEnabled = await Permission.location.isGranted; - MixpanelManager().setUserProperty('Location Enabled', SharedPreferencesUtil().locationEnabled); + AnalyticsManager().setUserAttribute('Location Enabled', SharedPreferencesUtil().locationEnabled); } context.read().periodicConnect('coming from HomePageWrapper'); await context.read().getInitialMemories(); diff --git a/app/lib/pages/onboarding/name/name_widget.dart b/app/lib/pages/onboarding/name/name_widget.dart index 73f6691ac..9d6b67fdd 100644 --- a/app/lib/pages/onboarding/name/name_widget.dart +++ b/app/lib/pages/onboarding/name/name_widget.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:gradient_borders/gradient_borders.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; class NameWidget extends StatefulWidget { final Function goNext; @@ -41,18 +42,12 @@ class _NameWidgetState extends State { enabled: true, focusNode: focusNode, controller: nameController, - // textCapitalization: TextCapitalization.sentences, obscureText: false, - // canRequestFocus: true, textAlign: TextAlign.center, textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( hintText: 'How Omi should call you?', - // label: const Text('What should Omi call you?'), hintStyle: const TextStyle(fontSize: 14, color: Colors.grey), - // border: UnderlineInputBorder( - // borderSide: BorderSide(color: Colors.grey.shade200), - // ), border: GradientOutlineInputBorder( borderRadius: BorderRadius.circular(8), gradient: const LinearGradient( @@ -107,6 +102,21 @@ class _NameWidgetState extends State { ) ], ), + const SizedBox( + height: 12, + ), + InkWell( + child: Text( + 'Need Help?', + style: TextStyle( + color: Colors.grey.shade300, + decoration: TextDecoration.underline, + ), + ), + onTap: () { + Intercom.instance.displayMessenger(); + }, + ), ], ), ); diff --git a/app/lib/pages/onboarding/permissions/permissions_widget.dart b/app/lib/pages/onboarding/permissions/permissions_widget.dart index d82999233..356e0b5e4 100644 --- a/app/lib/pages/onboarding/permissions/permissions_widget.dart +++ b/app/lib/pages/onboarding/permissions/permissions_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:friend_private/providers/onboarding_provider.dart'; import 'package:friend_private/widgets/dialog.dart'; import 'package:gradient_borders/box_borders/gradient_box_border.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; @@ -216,6 +217,21 @@ class _PermissionsWidgetState extends State { ) ], ), + const SizedBox( + height: 12, + ), + InkWell( + child: Text( + 'Need Help?', + style: TextStyle( + color: Colors.grey.shade300, + decoration: TextDecoration.underline, + ), + ), + onTap: () { + Intercom.instance.displayMessenger(); + }, + ), ], ), ); diff --git a/app/lib/pages/onboarding/welcome/page.dart b/app/lib/pages/onboarding/welcome/page.dart index 2b7126e63..cf93b3cd5 100644 --- a/app/lib/pages/onboarding/welcome/page.dart +++ b/app/lib/pages/onboarding/welcome/page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:friend_private/providers/onboarding_provider.dart'; +import 'package:friend_private/utils/analytics/intercom.dart'; import 'package:friend_private/widgets/dialog.dart'; import 'package:gradient_borders/box_borders/gradient_box_border.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -37,71 +38,88 @@ class _WelcomePageState extends State with SingleTickerProviderStat return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - Consumer(builder: (context, provider, child) { - return Padding( - padding: EdgeInsets.only(left: screenSize.width * 0.1, right: screenSize.width * 0.1), - child: Container( - 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: ElevatedButton( - onPressed: () async { - await provider.askForBluetoothPermissions(); - if (provider.hasBluetoothPermission) { - widget.goNext(); - } else { - showDialog( - context: context, - builder: (c) => getDialog( - context, - () { - Navigator.of(context).pop(); - openAppSettings(); - }, - () {}, - 'Permissions Required', - 'This app needs Bluetooth and Location permissions to function properly. Please enable them in the settings.', - okButtonText: 'Open Settings', - singleButton: true, - ), - barrierDismissible: false, - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - shadowColor: const Color.fromARGB(255, 17, 17, 17), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + Consumer( + builder: (context, provider, child) { + return Padding( + padding: EdgeInsets.only(left: screenSize.width * 0.1, right: screenSize.width * 0.1), + child: Container( + 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: Container( - width: double.infinity, // Button takes full width of the padding - height: 45, // Fixed height for the button - alignment: Alignment.center, - child: const Text( - 'Connect My Friend', - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 18, - color: Color.fromARGB(255, 255, 255, 255), + child: ElevatedButton( + onPressed: () async { + await provider.askForBluetoothPermissions(); + if (provider.hasBluetoothPermission) { + widget.goNext(); + } else { + showDialog( + context: context, + builder: (c) => getDialog( + context, + () { + Navigator.of(context).pop(); + openAppSettings(); + }, + () {}, + 'Permissions Required', + 'This app needs Bluetooth and Location permissions to function properly. Please enable them in the settings.', + okButtonText: 'Open Settings', + singleButton: true, + ), + barrierDismissible: false, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: const Color.fromARGB(255, 17, 17, 17), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Container( + width: double.infinity, // Button takes full width of the padding + height: 45, // Fixed height for the button + alignment: Alignment.center, + child: const Text( + 'Connect My Friend', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 18, + color: Color.fromARGB(255, 255, 255, 255), + ), ), ), ), ), + ); + }, + ), + const SizedBox( + height: 12, + ), + InkWell( + child: Text( + 'Need Help?', + style: TextStyle( + color: Colors.grey.shade300, + decoration: TextDecoration.underline, ), - ); - }), - const SizedBox(height: 16) + ), + onTap: () { + IntercomManager.instance.intercom.displayMessenger(); + }, + ), + const SizedBox(height: 10) ], ); } diff --git a/app/lib/pages/onboarding/wrapper.dart b/app/lib/pages/onboarding/wrapper.dart index 6f8259967..7763cba84 100644 --- a/app/lib/pages/onboarding/wrapper.dart +++ b/app/lib/pages/onboarding/wrapper.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:friend_private/backend/auth.dart'; import 'package:friend_private/backend/preferences.dart'; @@ -16,6 +17,7 @@ import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/onboarding_provider.dart'; import 'package:friend_private/providers/speech_profile_provider.dart'; import 'package:friend_private/services/services.dart'; +import 'package:friend_private/utils/analytics/intercom.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/widgets/device_widget.dart'; @@ -79,6 +81,9 @@ class _OnboardingWrapperState extends State with TickerProvid onSignIn: () { MixpanelManager().onboardingStepCompleted('Auth'); context.read().setupHasSpeakerProfile(); + IntercomManager.instance.intercom.loginIdentifiedUser( + userId: FirebaseAuth.instance.currentUser!.uid, + ); if (SharedPreferencesUtil().onboardingCompleted) { // previous users // Not needed anymore, because AuthProvider already does this @@ -90,6 +95,11 @@ class _OnboardingWrapperState extends State with TickerProvid ), NameWidget(goNext: () { _goNext(); + IntercomManager.instance.updateUser( + FirebaseAuth.instance.currentUser!.email, + FirebaseAuth.instance.currentUser!.displayName, + FirebaseAuth.instance.currentUser!.uid, + ); MixpanelManager().onboardingStepCompleted('Name'); }), PermissionsWidget( @@ -199,7 +209,7 @@ class _OnboardingWrapperState extends State with TickerProvid SizedBox( height: (_controller!.index == 5 || _controller!.index == 6 || _controller!.index == 7) ? max(MediaQuery.of(context).size.height - 500 - 10, maxHeightWithTextScale(context)) - : max(MediaQuery.of(context).size.height - 500 - 60, maxHeightWithTextScale(context)), + : max(MediaQuery.of(context).size.height - 500 - 30, maxHeightWithTextScale(context)), child: Padding( padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height <= 700 ? 10 : 64), child: TabBarView( diff --git a/app/lib/pages/settings/about.dart b/app/lib/pages/settings/about.dart index 85cc99d12..22bf0bb59 100644 --- a/app/lib/pages/settings/about.dart +++ b/app/lib/pages/settings/about.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:friend_private/pages/settings/webview.dart'; +import 'package:friend_private/utils/analytics/intercom.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/other/temp.dart'; -import 'package:intercom_flutter/intercom_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; class AboutOmiPage extends StatefulWidget { @@ -54,7 +54,7 @@ class _AboutOmiPageState extends State { contentPadding: const EdgeInsets.fromLTRB(4, 0, 24, 0), trailing: const Icon(Icons.help_outline_outlined, color: Colors.white, size: 20), onTap: () async { - await Intercom.instance.displayMessenger(); + await IntercomManager.instance.intercom.displayMessenger(); }, ), ListTile( diff --git a/app/lib/pages/settings/delete_account.dart b/app/lib/pages/settings/delete_account.dart new file mode 100644 index 000000000..a0dd5562b --- /dev/null +++ b/app/lib/pages/settings/delete_account.dart @@ -0,0 +1,187 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/http/api/users.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/main.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:gradient_borders/gradient_borders.dart'; + +class DeleteAccount extends StatefulWidget { + const DeleteAccount({super.key}); + + @override + State createState() => _DeleteAccountState(); +} + +class _DeleteAccountState extends State { + bool checkboxValue = false; + bool isDeleteing = false; + + void updateCheckboxValue(bool value) { + setState(() { + checkboxValue = value; + }); + } + + Future deleteAccountNow() async { + setState(() { + isDeleteing = true; + }); + MixpanelManager().deleteAccountConfirmed(); + MixpanelManager().deleteUser(); + SharedPreferencesUtil().clear(); + await deleteAccount(); + await FirebaseAuth.instance.signOut(); + setState(() { + isDeleteing = false; + }); + routeToPage(context, const DeciderWidget(), replace: true); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: isDeleteing, + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + title: const Text('Delete Account'), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + const SizedBox( + height: 10, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 50), + child: Text( + "Are you sure you want to delete your account?", + style: TextStyle( + fontSize: 24, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 10, + ), + const Text( + "This cannot be undone.", + style: TextStyle(fontSize: 18), + ), + const SizedBox( + height: 30, + ), + const ListTile( + leading: Icon(Icons.message_rounded), + title: Text("All of your memories and conversations will be permanently erased."), + ), + const ListTile( + leading: Icon(Icons.person_pin_outlined), + title: Text("Your plugins and integrations will be disconnected effectively immediately."), + ), + const ListTile( + leading: Icon(Icons.upload_file_outlined), + title: Text( + "You can export your data before deleting your account, but once deleted, it cannot be recovered."), + ), + const Spacer(), + Row( + children: [ + Checkbox( + value: checkboxValue, + onChanged: (value) { + if (value != null) { + updateCheckboxValue(value); + } + }, + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.80, + child: const Text( + "I understand that deleting my account is permanent and all data, including memories and conversations, will be lost and cannot be recovered. "), + ), + ], + ), + const SizedBox( + height: 30, + ), + isDeleteing + ? const CircularProgressIndicator( + color: Colors.white, + ) + : Container( + 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: ElevatedButton( + onPressed: () { + if (checkboxValue) { + showDialog( + context: context, + builder: (c) { + return getDialog(context, () { + MixpanelManager().deleteAccountCancelled(); + Navigator.of(context).pop(); + }, () { + deleteAccountNow(); + Navigator.of(context).pop(); + }, "Are you sure?\n", + "This action is irreversible and will permanently delete your account and all associated data. Are you sure you want to proceed?", + okButtonText: 'Delete Now', cancelButtonText: 'Go Back'); + }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Check the box to confirm you understand that deleting your account is permanent and irreversible.'), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: const Color.fromARGB(255, 17, 17, 17), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Container( + width: double.infinity, + height: 45, + alignment: Alignment.center, + child: const Text( + 'Delete Account', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 18, + color: Color.fromARGB(255, 255, 255, 255), + ), + ), + ), + ), + ), + const SizedBox( + height: 70, + ), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/pages/settings/device_settings.dart b/app/lib/pages/settings/device_settings.dart index 4c0964fca..9403de46b 100644 --- a/app/lib/pages/settings/device_settings.dart +++ b/app/lib/pages/settings/device_settings.dart @@ -7,10 +7,10 @@ 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/intercom.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:gradient_borders/gradient_borders.dart'; -import 'package:intercom_flutter/intercom_flutter.dart'; import 'package:provider/provider.dart'; class DeviceSettings extends StatefulWidget { @@ -96,7 +96,7 @@ class _DeviceSettingsState extends State { ), GestureDetector( onTap: () async { - await Intercom.instance.displayArticle('9907475-how-to-charge-the-device'); + await IntercomManager().displayChargingArticle(); }, child: const ListTile( title: Text('Issues charging the device?'), diff --git a/app/lib/pages/settings/profile.dart b/app/lib/pages/settings/profile.dart index bfe5233ef..0cc7c4a51 100644 --- a/app/lib/pages/settings/profile.dart +++ b/app/lib/pages/settings/profile.dart @@ -8,8 +8,8 @@ import 'package:friend_private/pages/settings/recordings_storage_permission.dart import 'package:friend_private/pages/speech_profile/page.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:url_launcher/url_launcher.dart'; + +import 'delete_account.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @@ -179,19 +179,7 @@ class _ProfilePageState extends State { ), onTap: () { MixpanelManager().pageOpened('Profile Delete Account Dialog'); - showDialog( - context: context, - builder: (ctx) { - return getDialog( - context, - () => Navigator.of(context).pop(), - () => launchUrl(Uri.parse('mailto:team@basedhardware.com?subject=Delete%20My%20Account')), - 'Deleting Account?', - 'Please send us an email at team@basedhardware.com', - okButtonText: 'Open Email', - singleButton: false, - ); - }); + Navigator.push(context, MaterialPageRoute(builder: (context) => const DeleteAccount())); }, ) ], diff --git a/app/lib/providers/home_provider.dart b/app/lib/providers/home_provider.dart index cc757665a..409ce03c8 100644 --- a/app/lib/providers/home_provider.dart +++ b/app/lib/providers/home_provider.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/api/speech_profile.dart'; import 'package:friend_private/backend/http/api/users.dart'; import 'package:friend_private/backend/preferences.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/utils/analytics/analytics_manager.dart'; class HomeProvider extends ChangeNotifier { int selectedIndex = 0; @@ -45,7 +45,7 @@ class HomeProvider extends ChangeNotifier { setSpeakerProfile(res); SharedPreferencesUtil().hasSpeakerProfile = res; debugPrint('_setupHasSpeakerProfile: ${SharedPreferencesUtil().hasSpeakerProfile}'); - MixpanelManager().setUserProperty('Speaker Profile', SharedPreferencesUtil().hasSpeakerProfile); + AnalyticsManager().setUserAttribute('Speaker Profile', SharedPreferencesUtil().hasSpeakerProfile); setIsLoading(false); notifyListeners(); } diff --git a/app/lib/providers/onboarding_provider.dart b/app/lib/providers/onboarding_provider.dart index 25ddbbd25..85af36013 100644 --- a/app/lib/providers/onboarding_provider.dart +++ b/app/lib/providers/onboarding_provider.dart @@ -14,7 +14,7 @@ import 'package:friend_private/providers/device_provider.dart'; import 'package:friend_private/services/devices.dart'; import 'package:friend_private/services/notifications.dart'; import 'package:friend_private/services/services.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/utils/analytics/analytics_manager.dart'; import 'package:friend_private/utils/audio/foreground.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -62,20 +62,20 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen void updateLocationPermission(bool value) { hasLocationPermission = value; SharedPreferencesUtil().locationEnabled = value; - MixpanelManager().setUserProperty('Location Enabled', SharedPreferencesUtil().locationEnabled); + AnalyticsManager().setUserAttribute('Location Enabled', SharedPreferencesUtil().locationEnabled); notifyListeners(); } void updateNotificationPermission(bool value) { hasNotificationPermission = value; SharedPreferencesUtil().notificationsEnabled = value; - MixpanelManager().setUserProperty('Notifications Enabled', SharedPreferencesUtil().notificationsEnabled); + AnalyticsManager().setUserAttribute('Notifications Enabled', SharedPreferencesUtil().notificationsEnabled); notifyListeners(); } void updateBackgroundPermission(bool value) { hasBackgroundPermission = value; - MixpanelManager().setUserProperty('Background Permission Enabled', hasBackgroundPermission); + AnalyticsManager().setUserAttribute('Background Permission Enabled', hasBackgroundPermission); notifyListeners(); } diff --git a/app/lib/utils/analytics/analytics_manager.dart b/app/lib/utils/analytics/analytics_manager.dart new file mode 100644 index 000000000..e03d285d1 --- /dev/null +++ b/app/lib/utils/analytics/analytics_manager.dart @@ -0,0 +1,22 @@ +import 'package:friend_private/utils/analytics/intercom.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; + +class AnalyticsManager { + static final AnalyticsManager _instance = AnalyticsManager._internal(); + + factory AnalyticsManager() { + return _instance; + } + + AnalyticsManager._internal(); + + void setUserAttributes() { + MixpanelManager().setPeopleValues(); + IntercomManager.instance.setUserAttributes(); + } + + void setUserAttribute(String key, dynamic value) { + MixpanelManager().setUserProperty(key, value); + IntercomManager.instance.updateCustomAttributes({key: value}); + } +} diff --git a/app/lib/utils/analytics/intercom.dart b/app/lib/utils/analytics/intercom.dart new file mode 100644 index 000000000..381ed9f4c --- /dev/null +++ b/app/lib/utils/analytics/intercom.dart @@ -0,0 +1,60 @@ +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/env/env.dart'; +import 'package:intercom_flutter/intercom_flutter.dart'; + +class IntercomManager { + static final IntercomManager _instance = IntercomManager._internal(); + static IntercomManager get instance => _instance; + static final SharedPreferencesUtil _preferences = SharedPreferencesUtil(); + + IntercomManager._internal(); + + Intercom get intercom => Intercom.instance; + + factory IntercomManager() { + return _instance; + } + + Future initIntercom() async { + if (Env.intercomAppId == null) return; + await intercom.initialize( + Env.intercomAppId!, + iosApiKey: Env.intercomIOSApiKey, + androidApiKey: Env.intercomAndroidApiKey, + ); + } + + Future displayChargingArticle() async { + return await intercom.displayArticle('9907475-how-to-charge-the-device'); + } + + Future logEvent(String eventName, {Map? metaData}) async { + return await intercom.logEvent(eventName, metaData); + } + + Future updateCustomAttributes(Map attributes) async { + return await intercom.updateUser(customAttributes: attributes); + } + + Future updateUser(String? email, String? name, String? uid) async { + return await intercom.updateUser( + email: email, + name: name, + userId: uid, + ); + } + + Future setUserAttributes() async { + await updateCustomAttributes({ + 'Notifications Enabled': _preferences.notificationsEnabled, + 'Location Enabled': _preferences.locationEnabled, + 'Plugins Enabled Count': _preferences.enabledPluginsCount, + 'Plugins Integrations Enabled Count': _preferences.enabledPluginsIntegrationsCount, + 'Speaker Profile': _preferences.hasSpeakerProfile, + 'Calendar Enabled': _preferences.calendarEnabled, + 'Recordings Language': _preferences.recordingsLanguage, + 'Authorized Storing Recordings': _preferences.permissionStoreRecordingsEnabled, + 'GCP Integration Set': _preferences.gcpCredentials.isNotEmpty && _preferences.gcpBucketName.isNotEmpty, + }); + } +} diff --git a/app/lib/utils/analytics/mixpanel.dart b/app/lib/utils/analytics/mixpanel.dart index ee1cff636..541be07a2 100644 --- a/app/lib/utils/analytics/mixpanel.dart +++ b/app/lib/utils/analytics/mixpanel.dart @@ -140,8 +140,7 @@ class MixpanelManager { void bottomNavigationTabClicked(String tab) => track('Bottom Navigation Tab Clicked', properties: {'tab': tab}); - void deviceConnected() => - track('Device Connected', properties: { + void deviceConnected() => track('Device Connected', properties: { ..._preferences.btDeviceStruct.toJson(fwverAsString: true), }); @@ -278,4 +277,12 @@ class MixpanelManager { void assignedSegment(String assignType) => track('Assigned Segment $assignType'); void unassignedSegment() => track('Unassigned Segment'); + + void deleteAccountClicked() => track('Delete Account Clicked'); + + void deleteAccountConfirmed() => track('Delete Account Confirmed'); + + void deleteAccountCancelled() => track('Delete Account Cancelled'); + + void deleteUser() => _mixpanel?.getPeople().deleteUser(); } diff --git a/app/lib/widgets/dialog.dart b/app/lib/widgets/dialog.dart index a668f8aca..c74538736 100644 --- a/app/lib/widgets/dialog.dart +++ b/app/lib/widgets/dialog.dart @@ -12,6 +12,7 @@ getDialog( String content, { bool singleButton = false, String okButtonText = 'Ok', + String cancelButtonText = 'Cancel', }) { var actions = singleButton ? [ @@ -23,7 +24,7 @@ getDialog( : [ TextButton( onPressed: () => onCancel(), - child: const Text('Cancel', style: TextStyle(color: Colors.white)), + child: Text(cancelButtonText, style: TextStyle(color: Colors.white)), ), TextButton( onPressed: () => onConfirm(), child: Text(okButtonText, style: const TextStyle(color: Colors.white))), diff --git a/backend/database/users.py b/backend/database/users.py index 830a214b3..c186461ea 100644 --- a/backend/database/users.py +++ b/backend/database/users.py @@ -43,3 +43,34 @@ def update_person(uid: str, person_id: str, name: str): def delete_person(uid: str, person_id: str): person_ref = db.collection('users').document(uid).collection('people').document(person_id) person_ref.update({'deleted': True}) + + +def delete_user_data(uid: str): + user_ref = db.collection('users').document(uid) + memories_ref = user_ref.collection('memories') + # delete all memories + batch = db.batch() + for doc in memories_ref.stream(): + batch.delete(doc.reference) + batch.commit() + # delete chat messages + messages_ref = user_ref.collection('messages') + batch = db.batch() + for doc in messages_ref.stream(): + batch.delete(doc.reference) + batch.commit() + # delete facts + batch = db.batch() + facts_ref = user_ref.collection('facts') + for doc in facts_ref.stream(): + batch.delete(doc.reference) + batch.commit() + # delete processing memories + processing_memories_ref = user_ref.collection('processing_memories') + batch = db.batch() + for doc in processing_memories_ref.stream(): + batch.delete(doc.reference) + batch.commit() + # delete user + user_ref.delete() + return {'status': 'ok', 'message': 'Account deleted successfully'} diff --git a/backend/routers/users.py b/backend/routers/users.py index f1a87dda8..7b449d808 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -17,11 +17,12 @@ @router.delete('/v1/users/delete-account', tags=['v1']) def delete_account(uid: str = Depends(auth.get_current_user_uid)): try: - # Set account as deleted in Firestore - update_user(uid, {"account_deleted": True}) - # TODO: delete user data from the database + delete_user_data(uid) + # delete user from firebase auth + auth.delete_account(uid) return {'status': 'ok', 'message': 'Account deleted successfully'} except Exception as e: + print('delete_account', str(e)) raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/utils/other/endpoints.py b/backend/utils/other/endpoints.py index bfd7ab50b..a93d5049b 100644 --- a/backend/utils/other/endpoints.py +++ b/backend/utils/other/endpoints.py @@ -86,3 +86,8 @@ def measure_time(*args, **kw): return result return measure_time + + +def delete_account(uid: str): + auth.delete_user(uid) + return {"message": "User deleted"} \ No newline at end of file