From 808473570c96bf490af4881c561e5caf25651f1b Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:08:45 +0530 Subject: [PATCH 01/19] creator profile widgets --- .../creator_profile/creator_profile.dart | 290 ++++++++++++++++++ .../creator_profile_provider.dart | 18 ++ app/lib/pages/settings/page.dart | 7 + 3 files changed, 315 insertions(+) create mode 100644 app/lib/pages/settings/creator_profile/creator_profile.dart create mode 100644 app/lib/pages/settings/creator_profile/creator_profile_provider.dart diff --git a/app/lib/pages/settings/creator_profile/creator_profile.dart b/app/lib/pages/settings/creator_profile/creator_profile.dart new file mode 100644 index 000000000..d102599b6 --- /dev/null +++ b/app/lib/pages/settings/creator_profile/creator_profile.dart @@ -0,0 +1,290 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/pages/settings/creator_profile/creator_profile_provider.dart'; +import 'package:provider/provider.dart'; + +class CreatorProfileWrapper extends StatelessWidget { + const CreatorProfileWrapper({super.key}); + + @override + Widget build(BuildContext context) { + return ListenableProvider( + create: (_) => CreatorProfileProvider(), + builder: (context, child) { + return Consumer( + builder: (context, provider, child) { + return CreatorProfile( + emailController: provider.creatorEmailController, + nameController: provider.creatorNameController, + paypalEmailController: provider.paypalEmailController, + paypalMeLinkController: provider.paypalMeLinkController, + ); + }, + ); + }, + ); + } +} + +class CreatorProfile extends StatelessWidget { + final TextEditingController emailController; + final TextEditingController nameController; + final TextEditingController paypalEmailController; + final TextEditingController paypalMeLinkController; + const CreatorProfile( + {super.key, + required this.emailController, + required this.nameController, + required this.paypalEmailController, + required this.paypalMeLinkController}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + elevation: 0, + title: const Text('Creator Profile'), + centerTitle: false, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + body: SingleChildScrollView( + child: Form( + key: context.read().formKey, + onChanged: () { + Provider.of(context, listen: false).checkValidations(); + }, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 6, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Creator Name', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + controller: nameController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter app name'; + } + return null; + }, + decoration: const InputDecoration( + error: null, + errorText: null, + isDense: true, + border: InputBorder.none, + hintText: 'Nik Shevchenko', + ), + ), + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Creator Email', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + controller: emailController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please provide a valid email'; + } + return null; + }, + decoration: const InputDecoration( + errorText: null, + error: null, + isDense: true, + border: InputBorder.none, + hintText: 'Nik@basedhardware.com', + ), + ), + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + const SizedBox( + width: 8, + ), + Icon( + Icons.info_outline, + color: Colors.grey.shade400, + size: 16, + ), + const SizedBox( + width: 8, + ), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.76, + child: Text( + 'This email will be public and used for communication.', + style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + ), + ), + ], + ) + ], + ), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 6, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'PayPal Email', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + controller: paypalEmailController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter app name'; + } + return null; + }, + decoration: const InputDecoration( + errorText: null, + isDense: true, + border: InputBorder.none, + hintText: 'Nik Shevchenko', + ), + ), + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'PayPal.Me Link', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + controller: paypalMeLinkController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please provide a valid email'; + } + return null; + }, + decoration: const InputDecoration( + errorText: null, + isDense: true, + border: InputBorder.none, + hintText: 'paypal.me/nikshevchenko', + ), + ), + ), + ], + ), + ) + ], + ), + ), + ), + ), + bottomNavigationBar: Container( + padding: const EdgeInsets.only(left: 30.0, right: 30, bottom: 50, top: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: Colors.grey.shade900, + gradient: LinearGradient( + colors: [Colors.black, Colors.black.withOpacity(0)], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + ), + child: GestureDetector( + onTap: () {}, + child: Container( + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: Colors.grey.shade700, + ), + child: const Text( + 'Save Details', + style: TextStyle(color: Colors.black, fontSize: 16), + textAlign: TextAlign.center, + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/pages/settings/creator_profile/creator_profile_provider.dart b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart new file mode 100644 index 000000000..c4028c29d --- /dev/null +++ b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class CreatorProfileProvider extends ChangeNotifier { + final TextEditingController creatorNameController = TextEditingController(); + final TextEditingController creatorEmailController = TextEditingController(); + final TextEditingController paypalEmailController = TextEditingController(); + final TextEditingController paypalMeLinkController = TextEditingController(); + final GlobalKey formKey = GlobalKey(); + bool isFormValid = false; + + void checkValidations() { + if (formKey.currentState!.validate()) { + isFormValid = true; + } else { + isFormValid = false; + } + } +} diff --git a/app/lib/pages/settings/page.dart b/app/lib/pages/settings/page.dart index b2376d64f..fc905d48f 100644 --- a/app/lib/pages/settings/page.dart +++ b/app/lib/pages/settings/page.dart @@ -3,6 +3,7 @@ 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/settings/about.dart'; +import 'package:friend_private/pages/settings/creator_profile/creator_profile.dart'; import 'package:friend_private/pages/settings/developer.dart'; import 'package:friend_private/pages/settings/profile.dart'; import 'package:friend_private/pages/settings/widgets.dart'; @@ -80,6 +81,12 @@ class _SettingsPageState extends State { () => routeToPage(context, const ProfilePage()), icon: Icons.person, ), + const SizedBox(height: 8), + getItemAddOn2( + 'Creator Profile', + () => routeToPage(context, const CreatorProfileWrapper()), + icon: Icons.person, + ), const SizedBox(height: 20), getItemAddOn2( 'Device Settings', From a860e141fac7af5b5d4c109ed2977ddfb4615f28 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:08:58 +0530 Subject: [PATCH 02/19] creator endpoint calls in frontend --- app/lib/backend/http/api/users.dart | 46 +++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/app/lib/backend/http/api/users.dart b/app/lib/backend/http/api/users.dart index 33e01e242..a8394d307 100644 --- a/app/lib/backend/http/api/users.dart +++ b/app/lib/backend/http/api/users.dart @@ -251,7 +251,6 @@ Future setMemorySummaryRating(String memoryId, int value, {String? reason} return response.statusCode == 200; } - Future setMessageResponseRating(String messageId, int value) async { var response = await makeApiCall( url: '${Env.apiBaseUrl}v1/users/analytics/chat_message?message_id=$messageId&value=$value', @@ -264,7 +263,6 @@ Future setMessageResponseRating(String messageId, int value) async { return response.statusCode == 200; } - Future getHasMemorySummaryRating(String memoryId) async { var response = await makeApiCall( url: '${Env.apiBaseUrl}v1/users/analytics/memory_summary?memory_id=$memoryId', @@ -282,3 +280,47 @@ Future getHasMemorySummaryRating(String memoryId) async { return false; } } + +Future saveCreatorProfile(String name, String email, String paypalEmail, String? paypalLink) async { + try { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/users/creator-profile', + headers: {}, + method: 'POST', + body: jsonEncode({ + 'name': name, + 'email': email, + 'paypal_email': paypalEmail, + 'paypal_link': paypalLink, + }), + ); + if (response == null) return false; + debugPrint('saveCreatorProfile response: ${response.body}'); + return response.statusCode == 200; + } catch (e) { + debugPrint('saveCreatorProfile error: $e'); + return false; + } +} + +Future updateCreatorProfile(String? name, String? email, String paypalEmail, String? paypalLink) async { + try { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/users/creator-profile', + headers: {}, + method: 'PATCH', + body: jsonEncode({ + 'name': name, + 'email': email, + 'paypal_email': paypalEmail, + 'paypal_link': paypalLink, + }), + ); + if (response == null) return false; + debugPrint('updateCreatorProfile response: ${response.body}'); + return response.statusCode == 200; + } catch (e) { + debugPrint('updateCreatorProfile error: $e'); + return false; + } +} From 7903d6cd1360c58f7a42f6a172811058c45d9328 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:09:17 +0530 Subject: [PATCH 03/19] creator endpoints on backend --- backend/database/users.py | 14 ++++++++++++++ backend/routers/users.py | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/backend/database/users.py b/backend/database/users.py index 20abc725e..02809cb36 100644 --- a/backend/database/users.py +++ b/backend/database/users.py @@ -79,6 +79,20 @@ def delete_user_data(uid: str): return {'status': 'ok', 'message': 'Account deleted successfully'} +# *********************************************** +# *************** CREATOR PROFILE *************** +# *********************************************** +def get_user_creator_profile_db(uid: str): + user_ref = db.collection('users').document(uid) + user_data = user_ref.get().to_dict() + return user_data.get('creator_profile', {}) + + +def set_user_creator_profile_db(uid: str, data: dict): + user_ref = db.collection('users').document(uid) + user_ref.set({'creator_profile': data}) + + # ************************************** # ************* Analytics ************** # ************************************** diff --git a/backend/routers/users.py b/backend/routers/users.py index 6ed96b1ac..b92269753 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -38,6 +38,29 @@ def set_user_geolocation(geolocation: Geolocation, uid: str = Depends(auth.get_c return {'status': 'ok'} +# *********************************************** +# *************** CREATOR PROFILE *************** +# *********************************************** + +@router.post('/v1/users/creator-profile', tags=['v1']) +def set_creator_profile(data: dict, uid: str = Depends(auth.get_current_user_uid)): + set_user_creator_profile_db(uid, data) + return {'status': 'ok'} + + +@router.get('/v1/users/creator-profile', tags=['v1']) +def get_creator_profile(uid: str = Depends(auth.get_current_user_uid)): + return get_user_creator_profile_db(uid) + + +@router.patch('/v1/users/creator-profile', tags=['v1']) +def update_creator_profile(data: dict, uid: str = Depends(auth.get_current_user_uid)): + current_data = get_user_creator_profile_db(uid) + current_data.update(data) + set_user_creator_profile_db(uid, current_data) + return {'status': 'ok'} + + # *********************************************** # ************* DEVELOPER WEBHOOKS ************** # *********************************************** From 96d899fb4313c8d252f0fb26a1e976f86d3fcb7f Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sat, 7 Dec 2024 21:25:09 +0530 Subject: [PATCH 04/19] improve profile endpoints --- backend/database/users.py | 2 +- backend/models/users.py | 9 +++++++++ backend/routers/users.py | 8 ++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/database/users.py b/backend/database/users.py index 02809cb36..dc7f53001 100644 --- a/backend/database/users.py +++ b/backend/database/users.py @@ -90,7 +90,7 @@ def get_user_creator_profile_db(uid: str): def set_user_creator_profile_db(uid: str, data: dict): user_ref = db.collection('users').document(uid) - user_ref.set({'creator_profile': data}) + user_ref.update({'creator_profile': data}) # ************************************** diff --git a/backend/models/users.py b/backend/models/users.py index 5ea200c1a..16ecd2822 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -1,5 +1,7 @@ from enum import Enum +from pydantic import BaseModel +from typing import Optional class WebhookType(str, Enum): audio_bytes = 'audio_bytes' @@ -7,3 +9,10 @@ class WebhookType(str, Enum): realtime_transcript = 'realtime_transcript' memory_created = 'memory_created', day_summary = 'day_summary' + + +class CreatorProfileRequest(BaseModel): + creator_name: str + creator_email: str + paypal_email: str + paypal_me_link: Optional[str] \ No newline at end of file diff --git a/backend/routers/users.py b/backend/routers/users.py index b92269753..620aabaf1 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -10,7 +10,7 @@ from database.users import * from models.memory import Geolocation, Memory from models.other import Person, CreatePerson -from models.users import WebhookType +from models.users import WebhookType, CreatorProfileRequest from utils.llm import followup_question_prompt from utils.other import endpoints as auth from utils.other.storage import delete_all_memory_recordings, get_user_person_speech_samples, \ @@ -43,7 +43,10 @@ def set_user_geolocation(geolocation: Geolocation, uid: str = Depends(auth.get_c # *********************************************** @router.post('/v1/users/creator-profile', tags=['v1']) -def set_creator_profile(data: dict, uid: str = Depends(auth.get_current_user_uid)): +def set_creator_profile(data: CreatorProfileRequest, uid: str = Depends(auth.get_current_user_uid)): + data = data.dict() + data['created_at'] = datetime.now(timezone.utc) + data['is_verified'] = False set_user_creator_profile_db(uid, data) return {'status': 'ok'} @@ -57,6 +60,7 @@ def get_creator_profile(uid: str = Depends(auth.get_current_user_uid)): def update_creator_profile(data: dict, uid: str = Depends(auth.get_current_user_uid)): current_data = get_user_creator_profile_db(uid) current_data.update(data) + current_data['updated_at'] = datetime.now(timezone.utc) set_user_creator_profile_db(uid, current_data) return {'status': 'ok'} From f7969d8857fb4499b602e4da15b0602be4973d8e Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:36:49 +0530 Subject: [PATCH 05/19] creator profile improvements --- app/lib/backend/http/api/users.dart | 43 +- app/lib/backend/schema/profile.dart | 49 ++ .../creator_profile/creator_profile.dart | 525 +++++++++--------- .../creator_profile_provider.dart | 87 ++- 4 files changed, 432 insertions(+), 272 deletions(-) create mode 100644 app/lib/backend/schema/profile.dart diff --git a/app/lib/backend/http/api/users.dart b/app/lib/backend/http/api/users.dart index a8394d307..0556f1ad5 100644 --- a/app/lib/backend/http/api/users.dart +++ b/app/lib/backend/http/api/users.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/shared.dart'; import 'package:friend_private/backend/schema/geolocation.dart'; import 'package:friend_private/backend/schema/person.dart'; +import 'package:friend_private/backend/schema/profile.dart'; import 'package:friend_private/env/env.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; @@ -281,18 +282,38 @@ Future getHasMemorySummaryRating(String memoryId) async { } } -Future saveCreatorProfile(String name, String email, String paypalEmail, String? paypalLink) async { +Future getCreatorProfile() async { + try { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/users/creator-profile', + headers: {}, + method: 'GET', + body: '', + ); + print(response?.body); + if (response == null) return null; + debugPrint('getCreatorProfile response: ${response.body}'); + if (response.statusCode == 200) { + Map json = jsonDecode(response.body); + if (json.isEmpty) { + return CreatorProfile.empty(); + } + return CreatorProfile.fromJson(json); + } + return null; + } catch (e) { + debugPrint('getCreatorProfile error: $e'); + return null; + } +} + +Future saveCreatorProfile(CreatorProfile profile) async { try { var response = await makeApiCall( url: '${Env.apiBaseUrl}v1/users/creator-profile', headers: {}, method: 'POST', - body: jsonEncode({ - 'name': name, - 'email': email, - 'paypal_email': paypalEmail, - 'paypal_link': paypalLink, - }), + body: jsonEncode(profile.toJson()), ); if (response == null) return false; debugPrint('saveCreatorProfile response: ${response.body}'); @@ -303,17 +324,17 @@ Future saveCreatorProfile(String name, String email, String paypalEmail, S } } -Future updateCreatorProfile(String? name, String? email, String paypalEmail, String? paypalLink) async { +Future updateCreatorProfileServer(String? name, String? email, String? paypalEmail, String? paypalLink) async { try { var response = await makeApiCall( url: '${Env.apiBaseUrl}v1/users/creator-profile', headers: {}, method: 'PATCH', body: jsonEncode({ - 'name': name, - 'email': email, + 'creator_name': name, + 'creator_email': email, 'paypal_email': paypalEmail, - 'paypal_link': paypalLink, + 'paypal_me_link': paypalLink, }), ); if (response == null) return false; diff --git a/app/lib/backend/schema/profile.dart b/app/lib/backend/schema/profile.dart new file mode 100644 index 000000000..9812a8872 --- /dev/null +++ b/app/lib/backend/schema/profile.dart @@ -0,0 +1,49 @@ +class CreatorProfile { + final String creatorName; + final String creatorEmail; + final String paypalEmail; + final String? paypalMeLink; + final bool? isVerified; + + CreatorProfile({ + required this.creatorName, + required this.creatorEmail, + required this.paypalEmail, + this.paypalMeLink, + this.isVerified, + }); + + factory CreatorProfile.fromJson(Map json) { + return CreatorProfile( + creatorName: json['creator_name'], + creatorEmail: json['creator_email'], + paypalEmail: json['paypal_email'], + paypalMeLink: json['paypal_me_link'] ?? '', + isVerified: json['is_verified'] ?? false, + ); + } + + Map toJson() { + return { + 'creator_name': creatorName, + 'creator_email': creatorEmail, + 'paypal_email': paypalEmail, + 'paypal_me_link': paypalMeLink ?? '', + 'is_verified': isVerified ?? false, + }; + } + + bool isEmpty() { + return creatorName.isEmpty && creatorEmail.isEmpty && paypalEmail.isEmpty; + } + + static CreatorProfile empty() { + return CreatorProfile( + creatorName: '', + creatorEmail: '', + paypalEmail: '', + paypalMeLink: '', + isVerified: false, + ); + } +} diff --git a/app/lib/pages/settings/creator_profile/creator_profile.dart b/app/lib/pages/settings/creator_profile/creator_profile.dart index d102599b6..6bba1edf5 100644 --- a/app/lib/pages/settings/creator_profile/creator_profile.dart +++ b/app/lib/pages/settings/creator_profile/creator_profile.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:friend_private/pages/settings/creator_profile/creator_profile_provider.dart'; +import 'package:friend_private/utils/other/text_formatters.dart'; +import 'package:friend_private/utils/other/validators.dart'; import 'package:provider/provider.dart'; class CreatorProfileWrapper extends StatelessWidget { @@ -8,283 +10,292 @@ class CreatorProfileWrapper extends StatelessWidget { @override Widget build(BuildContext context) { return ListenableProvider( - create: (_) => CreatorProfileProvider(), + create: (_) => CreatorProfileProvider()..getCreatorProfileDetails(), builder: (context, child) { - return Consumer( - builder: (context, provider, child) { - return CreatorProfile( - emailController: provider.creatorEmailController, - nameController: provider.creatorNameController, - paypalEmailController: provider.paypalEmailController, - paypalMeLinkController: provider.paypalMeLinkController, - ); - }, - ); + return const CreatorProfile(); }, ); } } class CreatorProfile extends StatelessWidget { - final TextEditingController emailController; - final TextEditingController nameController; - final TextEditingController paypalEmailController; - final TextEditingController paypalMeLinkController; - const CreatorProfile( - {super.key, - required this.emailController, - required this.nameController, - required this.paypalEmailController, - required this.paypalMeLinkController}); + const CreatorProfile({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.primary, - appBar: AppBar( + return Consumer(builder: (context, provider, child) { + return Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, - elevation: 0, - title: const Text('Creator Profile'), - centerTitle: false, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new), - onPressed: () { - Navigator.pop(context); - }, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + elevation: 0, + title: const Text('Creator Profile'), + centerTitle: false, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () { + Navigator.pop(context); + }, + ), ), - ), - body: SingleChildScrollView( - child: Form( - key: context.read().formKey, - onChanged: () { - Provider.of(context, listen: false).checkValidations(); - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.all(14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 6, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Creator Name', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - controller: nameController, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter app name'; - } - return null; - }, - decoration: const InputDecoration( - error: null, - errorText: null, - isDense: true, - border: InputBorder.none, - hintText: 'Nik Shevchenko', + body: provider.isLoading + ? const Center( + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : SingleChildScrollView( + child: Form( + key: provider.formKey, + onChanged: () { + provider.submitButtonStatus(); + }, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), ), - ), - ), - const SizedBox( - height: 16, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Creator Email', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - controller: emailController, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please provide a valid email'; - } - return null; - }, - decoration: const InputDecoration( - errorText: null, - error: null, - isDense: true, - border: InputBorder.none, - hintText: 'Nik@basedhardware.com', + padding: const EdgeInsets.all(14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 6, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Creator Name', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + controller: provider.creatorNameController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please provide a valid name'; + } + return null; + }, + decoration: const InputDecoration( + error: null, + errorText: null, + isDense: true, + border: InputBorder.none, + hintText: 'Nik Shevchenko', + ), + ), + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'Creator Email', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + controller: provider.creatorEmailController, + inputFormatters: [ + LowercaseTextInputFormatter(), + ], + validator: (value) { + if (value == null || !isValidEmail(value)) { + return 'Please provide a valid Email Address for communication'; + } + return null; + }, + decoration: const InputDecoration( + errorText: null, + error: null, + isDense: true, + border: InputBorder.none, + hintText: 'Nik@basedhardware.com', + ), + ), + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + const SizedBox( + width: 8, + ), + Icon( + Icons.info_outline, + color: Colors.grey.shade400, + size: 16, + ), + const SizedBox( + width: 8, + ), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.76, + child: Text( + 'This email will be public and used for communication.', + style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + ), + ), + ], + ) + ], ), ), - ), - const SizedBox( - height: 2, - ), - Row( - children: [ - const SizedBox( - width: 8, - ), - Icon( - Icons.info_outline, - color: Colors.grey.shade400, - size: 16, - ), - const SizedBox( - width: 8, + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12.0), ), - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.76, - child: Text( - 'This email will be public and used for communication.', - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), - ), + padding: const EdgeInsets.all(14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 6, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'PayPal Email', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + controller: provider.paypalEmailController, + inputFormatters: [ + LowercaseTextInputFormatter(), + ], + validator: (value) { + if (value == null || !isValidEmail(value)) { + return 'Please provide a valid PayPal Email'; + } + return null; + }, + decoration: const InputDecoration( + errorText: null, + isDense: true, + border: InputBorder.none, + hintText: 'Nik Shevchenko', + ), + ), + ), + const SizedBox( + height: 16, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'PayPal.Me Link', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(10.0), + ), + width: double.infinity, + child: TextFormField( + inputFormatters: [ + LowercaseTextInputFormatter(), + ], + controller: provider.paypalMeLinkController, + validator: (value) { + if (value == null || !isValidPayPalMeLink(value)) { + return 'Please provide a valid PayPal.Me Link'; + } + return null; + }, + decoration: const InputDecoration( + errorText: null, + isDense: true, + border: InputBorder.none, + hintText: 'paypal.me/nikshevchenko', + ), + ), + ), + ], ), - ], - ) - ], + ) + ], + ), ), ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(12.0), + ), + bottomNavigationBar: provider.isLoading + ? null + : Container( + padding: const EdgeInsets.only(left: 30.0, right: 30, bottom: 50, top: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: Colors.grey.shade900, + gradient: LinearGradient( + colors: [Colors.black, Colors.black.withOpacity(0)], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, ), - padding: const EdgeInsets.all(14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 6, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'PayPal Email', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - controller: paypalEmailController, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter app name'; - } - return null; - }, - decoration: const InputDecoration( - errorText: null, - isDense: true, - border: InputBorder.none, - hintText: 'Nik Shevchenko', - ), - ), - ), - const SizedBox( - height: 16, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'PayPal.Me Link', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - controller: paypalMeLinkController, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please provide a valid email'; - } - return null; - }, - decoration: const InputDecoration( - errorText: null, - isDense: true, - border: InputBorder.none, - hintText: 'paypal.me/nikshevchenko', - ), - ), - ), - ], + ), + child: GestureDetector( + onTap: provider.showSubmitButton + ? () async { + if (provider.profileExists) { + await provider.updateDetails(); + } else { + await provider.saveDetails(); + } + } + : null, + child: Container( + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: provider.showSubmitButton ? Colors.white : Colors.grey.shade700, + ), + child: const Text( + 'Save Details', + style: TextStyle(color: Colors.black, fontSize: 16), + textAlign: TextAlign.center, + ), ), - ) - ], - ), - ), - ), - ), - bottomNavigationBar: Container( - padding: const EdgeInsets.only(left: 30.0, right: 30, bottom: 50, top: 10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - color: Colors.grey.shade900, - gradient: LinearGradient( - colors: [Colors.black, Colors.black.withOpacity(0)], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - child: GestureDetector( - onTap: () {}, - child: Container( - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - color: Colors.grey.shade700, - ), - child: const Text( - 'Save Details', - style: TextStyle(color: Colors.black, fontSize: 16), - textAlign: TextAlign.center, - ), - ), - ), - ), - ); + ), + ), + ); + }); } } diff --git a/app/lib/pages/settings/creator_profile/creator_profile_provider.dart b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart index c4028c29d..35369c4f8 100644 --- a/app/lib/pages/settings/creator_profile/creator_profile_provider.dart +++ b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart @@ -1,4 +1,9 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:friend_private/backend/http/api/users.dart'; +import 'package:friend_private/backend/schema/profile.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; class CreatorProfileProvider extends ChangeNotifier { final TextEditingController creatorNameController = TextEditingController(); @@ -6,13 +11,87 @@ class CreatorProfileProvider extends ChangeNotifier { final TextEditingController paypalEmailController = TextEditingController(); final TextEditingController paypalMeLinkController = TextEditingController(); final GlobalKey formKey = GlobalKey(); - bool isFormValid = false; + bool showSubmitButton = false; + bool isLoading = false; + bool profileExists = false; + + void setIsLoading(bool value) { + isLoading = value; + notifyListeners(); + } + + void submitButtonStatus() { + if (creatorNameController.text.isNotEmpty && + creatorEmailController.text.isNotEmpty && + paypalEmailController.text.isNotEmpty && + paypalMeLinkController.text.isNotEmpty) { + showSubmitButton = true; + } else { + showSubmitButton = false; + } + notifyListeners(); + } + + Future getCreatorProfileDetails() async { + var res = await getCreatorProfile(); + if (res != null) { + if (res.isEmpty()) { + AppSnackbar.showSnackbarInfo('Looks like you have not created your Creator Profile yet'); + } else { + profileExists = true; + creatorNameController.text = res.creatorName; + creatorEmailController.text = res.creatorEmail; + paypalEmailController.text = res.paypalEmail; + paypalMeLinkController.text = res.paypalMeLink ?? ''; + } + showSubmitButton = false; + } else { + AppSnackbar.showSnackbarError('Failed to fetch your Creator Profile details'); + } + } + + Future updateDetails() async { + if (formKey.currentState!.validate()) { + setIsLoading(true); + var res = await updateCreatorProfileServer( + creatorNameController.text, + creatorEmailController.text, + paypalEmailController.text, + paypalMeLinkController.text, + ); + if (res) { + AppSnackbar.showSnackbarSuccess('Creator Profile updated successfully'); + } else { + AppSnackbar.showSnackbarError('Failed to update Creator Profile'); + } + showSubmitButton = false; + setIsLoading(false); + } else { + AppSnackbar.showSnackbarError('Please fill all the fields correctly'); + } + } - void checkValidations() { + Future saveDetails() async { if (formKey.currentState!.validate()) { - isFormValid = true; + setIsLoading(true); + var profile = CreatorProfile( + creatorName: creatorNameController.text, + creatorEmail: creatorEmailController.text, + paypalEmail: paypalEmailController.text, + paypalMeLink: paypalMeLinkController.text, + isVerified: false, + ); + var res = await saveCreatorProfile(profile); + if (res) { + profileExists = true; + AppSnackbar.showSnackbarSuccess('Creator Profile saved successfully'); + } else { + AppSnackbar.showSnackbarError('Failed to update Creator Profile'); + } + showSubmitButton = false; + setIsLoading(false); } else { - isFormValid = false; + AppSnackbar.showSnackbarError('Please fill all the fields correctly'); } } } From 78bb8a01960356da35e5e47562f393fc67b69783 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:37:01 +0530 Subject: [PATCH 06/19] add paypal me link validator --- app/lib/utils/other/validators.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/lib/utils/other/validators.dart b/app/lib/utils/other/validators.dart index edbd6a0d1..aaca37425 100644 --- a/app/lib/utils/other/validators.dart +++ b/app/lib/utils/other/validators.dart @@ -18,3 +18,8 @@ bool isValidEmail(String email) { const emailPattern = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'; return RegExp(emailPattern).hasMatch(email); } + +bool isValidPayPalMeLink(String url) { + const payPalMePattern = r'^(?:https?:\/\/)?(?:www\.)?paypal\.me\/[a-zA-Z0-9]{1,100}$'; + return RegExp(payPalMePattern).hasMatch(url); +} From 5229b6db4f7a32c84fb17d70665bc7c7e77631e8 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:37:21 +0530 Subject: [PATCH 07/19] add lowecase text formatter and info snackbar --- app/lib/utils/alerts/app_snackbar.dart | 8 ++++++++ app/lib/utils/other/text_formatters.dart | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100644 app/lib/utils/other/text_formatters.dart diff --git a/app/lib/utils/alerts/app_snackbar.dart b/app/lib/utils/alerts/app_snackbar.dart index 30bd7c7a8..5dff1d021 100644 --- a/app/lib/utils/alerts/app_snackbar.dart +++ b/app/lib/utils/alerts/app_snackbar.dart @@ -27,4 +27,12 @@ class AppSnackbar { duration: duration, ); } + + static void showSnackbarInfo(String message, {Duration? duration}) { + showSnackbar( + message, + color: Colors.grey.shade800, + duration: duration, + ); + } } diff --git a/app/lib/utils/other/text_formatters.dart b/app/lib/utils/other/text_formatters.dart new file mode 100644 index 000000000..681bff001 --- /dev/null +++ b/app/lib/utils/other/text_formatters.dart @@ -0,0 +1,11 @@ +import 'package:flutter/services.dart'; + +class LowercaseTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + return TextEditingValue( + text: newValue.text.toLowerCase(), + selection: newValue.selection, + ); + } +} From db49fdeba2555c35cf865a56f8f2071c2890ad01 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:40:13 +0530 Subject: [PATCH 08/19] submit app v2 endpoint and models --- app/lib/backend/http/api/apps.dart | 2 +- app/lib/pages/apps/add_app.dart | 2 - .../apps/providers/add_app_provider.dart | 15 ---- app/lib/pages/apps/update_app.dart | 2 - .../apps/widgets/app_metadata_widget.dart | 73 ------------------- backend/models/app.py | 19 +++++ backend/routers/apps.py | 35 ++++++++- 7 files changed, 54 insertions(+), 94 deletions(-) diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index e98539148..99e2aa122 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -160,7 +160,7 @@ Future isAppSetupCompleted(String? url) async { Future submitAppServer(File file, Map appData) async { var request = http.MultipartRequest( 'POST', - Uri.parse('${Env.apiBaseUrl}v1/apps'), + Uri.parse('${Env.apiBaseUrl}v2/apps'), ); request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path))); request.headers.addAll({'Authorization': await getAuthHeader()}); diff --git a/app/lib/pages/apps/add_app.dart b/app/lib/pages/apps/add_app.dart index d2ddbfd33..283e78f45 100644 --- a/app/lib/pages/apps/add_app.dart +++ b/app/lib/pages/apps/add_app.dart @@ -101,8 +101,6 @@ class _AddAppPageState extends State { }, appNameController: provider.appNameController, appDescriptionController: provider.appDescriptionController, - creatorNameController: provider.creatorNameController, - creatorEmailController: provider.creatorEmailController, categories: provider.categories, setAppCategory: provider.setAppCategory, imageFile: provider.imageFile, diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart index a5d41f2fb..76108a000 100644 --- a/app/lib/pages/apps/providers/add_app_provider.dart +++ b/app/lib/pages/apps/providers/add_app_provider.dart @@ -22,8 +22,6 @@ class AddAppProvider extends ChangeNotifier { TextEditingController appNameController = TextEditingController(); TextEditingController appDescriptionController = TextEditingController(); - TextEditingController creatorNameController = TextEditingController(); - TextEditingController creatorEmailController = TextEditingController(); TextEditingController chatPromptController = TextEditingController(); TextEditingController memoryPromptController = TextEditingController(); String? appCategory; @@ -65,8 +63,6 @@ class AddAppProvider extends ChangeNotifier { if (capabilities.isEmpty) { await getAppCapabilities(); } - creatorNameController.text = SharedPreferencesUtil().givenName; - creatorEmailController.text = SharedPreferencesUtil().email; setIsLoading(false); } @@ -99,8 +95,6 @@ class AddAppProvider extends ChangeNotifier { imageUrl = app.image; appNameController.text = app.name.decodeString; appDescriptionController.text = app.description.decodeString; - creatorNameController.text = app.author.decodeString; - creatorEmailController.text = app.email ?? ''; makeAppPublic = !app.private; selectedCapabilities = app.getCapabilitiesFromIds(capabilities); if (app.externalIntegration != null) { @@ -130,8 +124,6 @@ class AddAppProvider extends ChangeNotifier { void clear() { appNameController.clear(); appDescriptionController.clear(); - creatorNameController.clear(); - creatorEmailController.clear(); chatPromptController.clear(); memoryPromptController.clear(); triggerEvent = null; @@ -185,9 +177,6 @@ class AddAppProvider extends ChangeNotifier { if (appDescriptionController.text != app.description) { return true; } - if (creatorNameController.text != app.author) { - return true; - } if (makeAppPublic != !app.private) { return true; } @@ -340,8 +329,6 @@ class AddAppProvider extends ChangeNotifier { Map data = { 'name': appNameController.text, 'description': appDescriptionController.text, - 'author': creatorNameController.text, - 'email': creatorEmailController.text, 'capabilities': selectedCapabilities.map((e) => e.id).toList(), 'deleted': false, 'uid': SharedPreferencesUtil().uid, @@ -398,8 +385,6 @@ class AddAppProvider extends ChangeNotifier { Map data = { 'name': appNameController.text, 'description': appDescriptionController.text, - 'author': creatorNameController.text, - 'email': creatorEmailController.text, 'capabilities': selectedCapabilities.map((e) => e.id).toList(), 'deleted': false, 'uid': SharedPreferencesUtil().uid, diff --git a/app/lib/pages/apps/update_app.dart b/app/lib/pages/apps/update_app.dart index d48b2e3bd..ced0edef1 100644 --- a/app/lib/pages/apps/update_app.dart +++ b/app/lib/pages/apps/update_app.dart @@ -103,8 +103,6 @@ class _UpdateAppPageState extends State { imageFile: provider.imageFile, appNameController: provider.appNameController, appDescriptionController: provider.appDescriptionController, - creatorNameController: provider.creatorNameController, - creatorEmailController: provider.creatorEmailController, categories: provider.categories, setAppCategory: provider.setAppCategory, imageUrl: provider.imageUrl, diff --git a/app/lib/pages/apps/widgets/app_metadata_widget.dart b/app/lib/pages/apps/widgets/app_metadata_widget.dart index f561fdb13..f052f6541 100644 --- a/app/lib/pages/apps/widgets/app_metadata_widget.dart +++ b/app/lib/pages/apps/widgets/app_metadata_widget.dart @@ -12,8 +12,6 @@ class AppMetadataWidget extends StatelessWidget { final VoidCallback pickImage; final TextEditingController appNameController; final TextEditingController appDescriptionController; - final TextEditingController creatorNameController; - final TextEditingController creatorEmailController; final List categories; final Function(String?) setAppCategory; final String? category; @@ -25,8 +23,6 @@ class AppMetadataWidget extends StatelessWidget { required this.pickImage, required this.appNameController, required this.appDescriptionController, - required this.creatorNameController, - required this.creatorEmailController, required this.categories, required this.setAppCategory, this.category, @@ -261,75 +257,6 @@ class AppMetadataWidget extends StatelessWidget { const SizedBox( height: 16, ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Creator Name', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter creator name'; - } - return null; - }, - controller: creatorNameController, - decoration: const InputDecoration( - contentPadding: EdgeInsets.only(top: 6, bottom: 6), - isDense: true, - errorText: null, - border: InputBorder.none, - hintText: 'Nik Shevchenko', - ), - ), - ), - const SizedBox( - height: 16, - ), - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - 'Email Address', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - margin: const EdgeInsets.only(left: 2.0, right: 2.0, top: 10, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(10.0), - ), - width: double.infinity, - child: TextFormField( - controller: creatorEmailController, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter creator email'; - } - return null; - }, - decoration: const InputDecoration( - contentPadding: EdgeInsets.only(top: 6, bottom: 6), - isDense: true, - border: InputBorder.none, - hintText: 'nik@basedhardware.com', - ), - ), - ), - const SizedBox( - height: 16, - ), Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( diff --git a/backend/models/app.py b/backend/models/app.py index 3622c1536..ae00df804 100644 --- a/backend/models/app.py +++ b/backend/models/app.py @@ -117,3 +117,22 @@ class UsageHistoryItem(BaseModel): memory_id: Optional[str] = None timestamp: datetime type: UsageHistoryType + + +# ****************************************************** +# ***************** APP REQUEST MODELS ***************** +# ****************************************************** + +class SubmitAppRequest(BaseModel): + name: str + private: bool = False + approved: bool = False + status: str = 'under-review' + category: str + description: str + capabilities: Set[str] + memory_prompt: Optional[str] = None + chat_prompt: Optional[str] = None + external_integration: Optional[ExternalIntegration] = None + deleted: bool = False + proactive_notification: Optional[ProactiveNotification] = None \ No newline at end of file diff --git a/backend/routers/apps.py b/backend/routers/apps.py index ce1884ba9..c6dd185fe 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from typing import List import requests +from pydantic import ValidationError from ulid import ULID from fastapi import APIRouter, Depends, Form, UploadFile, File, HTTPException, Header @@ -18,7 +19,7 @@ from utils.notifications import send_notification from utils.other import endpoints as auth -from models.app import App +from models.app import App, SubmitAppRequest from utils.other.storage import upload_plugin_logo, delete_plugin_logo router = APIRouter() @@ -68,6 +69,38 @@ def submit_app(app_data: str = Form(...), file: UploadFile = File(...), uid=Depe return {'status': 'ok'} +@router.post('/v2/apps', tags=['v2']) +def submit_app_v2(app_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)): + try: + parsed_data = json.loads(app_data) + validated_data = SubmitAppRequest(**parsed_data) + except (json.JSONDecodeError, ValidationError) as e: + raise HTTPException(status_code=422, detail=str(e)) + data = validated_data.dict() + data['name'] = data['name'].strip() + data['id'] = str(ULID()) + if external_integration := data.get('external_integration'): + if external_integration.get('triggers_on') is None: + raise HTTPException(status_code=422, detail='Triggers on is required') + # check if setup_instructions_file_path is a single url or a just a string of text + if external_integration.get('setup_instructions_file_path'): + external_integration['setup_instructions_file_path'] = external_integration[ + 'setup_instructions_file_path'].strip() + if external_integration['setup_instructions_file_path'].startswith('http'): + external_integration['is_instructions_url'] = True + else: + external_integration['is_instructions_url'] = False + os.makedirs(f'_temp/plugins', exist_ok=True) + file_path = f"_temp/plugins/{file.filename}" + with open(file_path, 'wb') as f: + f.write(file.file.read()) + imgUrl = upload_plugin_logo(file_path, data['id']) + data['image'] = imgUrl + data['created_at'] = datetime.now(timezone.utc) + add_app_to_db(data) + return {'status': 'ok'} + + @router.patch('/v1/apps/{app_id}', tags=['v1']) def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(None), uid=Depends(auth.get_current_user_uid)): From 4a446c47c10e44f5ea6682f82b922a400a9c40ac Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:30:04 +0530 Subject: [PATCH 09/19] add multi apps usage funcs and stats endpoint --- backend/database/redis_db.py | 43 ++++++++++++++++++++++++++++++++++++ backend/routers/users.py | 18 +++++++++++++++ backend/utils/apps.py | 18 +++++++++++++-- 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/backend/database/redis_db.py b/backend/database/redis_db.py index 86310a4c1..2229615c6 100644 --- a/backend/database/redis_db.py +++ b/backend/database/redis_db.py @@ -85,6 +85,34 @@ def get_app_usage_count_cache(app_id: str) -> int | None: return eval(count) +def get_multiple_apps_usage_count_cache(app_ids: list) -> dict: + if not app_ids: + return {} + + keys = [f'apps:{app_id}:usage_count' for app_id in app_ids] + counts = r.mget(keys) + if counts is None: + return {} + return { + app_id: int(count) if count else 0 + for app_id, count in zip(app_ids, counts) + } + + +def get_multiple_apps_money_made_amount_cache(app_ids: list) -> dict: + if not app_ids: + return {} + + keys = [f'apps:{app_id}:money_made' for app_id in app_ids] + amounts = r.mget(keys) + if amounts is None: + return {} + return { + app_id: eval(amount) if amount else 0 + for app_id, amount in zip(app_ids, amounts) + } + + def set_app_money_made_amount_cache(app_id: str, amount: float): r.set(f'apps:{app_id}:money_made', amount, ex=60 * 15) # 15 minutes @@ -159,6 +187,21 @@ def enable_app(uid: str, app_id: str): r.sadd(f'users:{uid}:enabled_plugins', app_id) +def get_app_enabled_count(app_id: str) -> int: + count = r.scard(f'users:*:enabled_plugins') + return count + + +def get_multiple_apps_enabled_count_cache(app_ids: list) -> dict: + if not app_ids: + return {} + + counts = {} + for app_id in app_ids: + counts[app_id] = r.scard(f'users:*:enabled_plugins') + return counts + + def disable_app(uid: str, app_id: str): r.srem(f'users:{uid}:enabled_plugins', app_id) diff --git a/backend/routers/users.py b/backend/routers/users.py index 620aabaf1..6fd08e389 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException +from database.apps import get_user_public_apps_db from database.memories import get_in_progress_memory, get_memory from database.redis_db import cache_user_geolocation, set_user_webhook_db, get_user_webhook_db, disable_user_webhook_db, \ enable_user_webhook_db, user_webhook_status_db @@ -11,6 +12,8 @@ from models.memory import Geolocation, Memory from models.other import Person, CreatePerson from models.users import WebhookType, CreatorProfileRequest +from utils.apps import get_multiple_apps_usage_count, get_multiple_apps_money_made_amount, \ + get_multiple_apps_enabled_count from utils.llm import followup_question_prompt from utils.other import endpoints as auth from utils.other.storage import delete_all_memory_recordings, get_user_person_speech_samples, \ @@ -65,6 +68,21 @@ def update_creator_profile(data: dict, uid: str = Depends(auth.get_current_user_ return {'status': 'ok'} +@router.get('/v1/users/creator-stats', tags=['v1']) +def get_creator_stats(uid: str = Depends(auth.get_current_user_uid)): + apps = get_user_public_apps_db(uid) + app_ids = [app['id'] for app in apps] + usage_count = get_multiple_apps_usage_count(app_ids) + money_made = get_multiple_apps_money_made_amount(app_ids) + users_count = get_multiple_apps_enabled_count(app_ids) + return { + 'usage_count': usage_count, + 'money_made': money_made, + 'apps_count': app_ids, + 'active_users': users_count, + } + + # *********************************************** # ************* DEVELOPER WEBHOOKS ************** # *********************************************** diff --git a/backend/utils/apps.py b/backend/utils/apps.py index 8121d8ff9..80ab8e542 100644 --- a/backend/utils/apps.py +++ b/backend/utils/apps.py @@ -11,7 +11,8 @@ set_generic_cache, set_app_usage_history_cache, get_app_usage_history_cache, get_app_money_made_cache, \ set_app_money_made_cache, get_plugins_installs_count, get_plugins_reviews, get_app_cache_by_id, set_app_cache_by_id, \ set_app_review_cache, get_app_usage_count_cache, set_app_money_made_amount_cache, get_app_money_made_amount_cache, \ - set_app_usage_count_cache + set_app_usage_count_cache, get_multiple_apps_usage_count_cache, get_multiple_apps_money_made_amount_cache, \ + get_multiple_apps_enabled_count_cache from models.app import App, UsageHistoryItem, UsageHistoryType @@ -42,6 +43,7 @@ def add_app_access_for_tester(app_id: str, uid: str): def remove_app_access_for_tester(app_id: str, uid: str): remove_app_access_for_tester_db(app_id, uid) + # ******************************** def weighted_rating(plugin): @@ -52,6 +54,18 @@ def weighted_rating(plugin): return (v / (v + m) * R) + (m / (v + m) * C) +def get_multiple_apps_usage_count(app_ids: List[str]) -> dict: + return get_multiple_apps_usage_count_cache(app_ids) + + +def get_multiple_apps_money_made_amount(app_ids: List[str]) -> dict: + return get_multiple_apps_money_made_amount_cache(app_ids) + + +def get_multiple_apps_enabled_count(app_ids: List[str]) -> dict: + return get_multiple_apps_enabled_count_cache(app_ids) + + def get_available_apps(uid: str, include_reviews: bool = False) -> List[App]: private_data = [] public_approved_data = [] @@ -160,7 +174,7 @@ def get_approved_available_apps(include_reviews: bool = False) -> list[App]: apps = [] for app in all_apps: app_dict = app - app_dict['installs'] = plugins_install.get(app['id'],0) + app_dict['installs'] = plugins_install.get(app['id'], 0) if include_reviews: reviews = plugins_review.get(app['id'], {}) sorted_reviews = reviews.values() From d17591c260a62ec8449843e5b12d003ba1c19857 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:31:05 +0530 Subject: [PATCH 10/19] dashboard ui and stats and api calls --- .../creator_profile/creator_dashboard.dart | 194 ++++++++++++++++++ ...file.dart => creator_profile_details.dart} | 20 +- .../creator_profile_provider.dart | 33 ++- app/lib/pages/settings/page.dart | 6 +- 4 files changed, 229 insertions(+), 24 deletions(-) create mode 100644 app/lib/pages/settings/creator_profile/creator_dashboard.dart rename app/lib/pages/settings/creator_profile/{creator_profile.dart => creator_profile_details.dart} (96%) diff --git a/app/lib/pages/settings/creator_profile/creator_dashboard.dart b/app/lib/pages/settings/creator_profile/creator_dashboard.dart new file mode 100644 index 000000000..372861a71 --- /dev/null +++ b/app/lib/pages/settings/creator_profile/creator_dashboard.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:friend_private/gen/assets.gen.dart'; +import 'package:friend_private/pages/settings/creator_profile/creator_profile_details.dart'; +import 'package:friend_private/pages/settings/creator_profile/creator_profile_provider.dart'; +import 'package:friend_private/utils/other/temp.dart'; +import 'package:provider/provider.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class CreatorDashboard extends StatefulWidget { + const CreatorDashboard({super.key}); + + @override + State createState() => _CreatorDashboardState(); +} + +class _CreatorDashboardState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await Provider.of(context, listen: false).getCreatorProfileDetails(); + await Provider.of(context, listen: false).getCreatorStats(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + elevation: 0, + title: const Text('Creator Dashboard'), + centerTitle: false, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + body: Consumer(builder: (context, provider, child) { + return SingleChildScrollView( + child: Skeletonizer( + enabled: provider.isLoading, + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Row( + children: [ + Skeleton.shade(child: SvgPicture.asset(Assets.images.icMoney, width: 38)), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("\$${provider.totalEarnings}", + style: const TextStyle(color: Colors.white, fontSize: 20)), + const SizedBox(height: 4), + const Text("Money Earned", style: TextStyle(color: Colors.white, fontSize: 12)), + ], + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Row( + children: [ + Skeleton.shade(child: SvgPicture.asset(Assets.images.icChart2, width: 38)), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(provider.totalUsage.toString(), + style: const TextStyle(color: Colors.white, fontSize: 20)), + const SizedBox(height: 4), + const Text("Times Used", style: TextStyle(color: Colors.white, fontSize: 12)), + ], + ), + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + width: MediaQuery.sizeOf(context).width * 0.46, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Row( + children: [ + Skeleton.shade(child: SvgPicture.asset(Assets.images.icApps, width: 36)), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(provider.publishedApps.toString(), + style: const TextStyle(color: Colors.white, fontSize: 18)), + const SizedBox(height: 4), + const Text("Published Apps", style: TextStyle(color: Colors.white, fontSize: 12)), + ], + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + width: MediaQuery.sizeOf(context).width * 0.46, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Row( + children: [ + Skeleton.shade(child: SvgPicture.asset(Assets.images.icUsers, width: 36)), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(provider.totalUsers.toString(), + style: const TextStyle(color: Colors.white, fontSize: 18)), + const SizedBox(height: 4), + const Text("Active Users", style: TextStyle(color: Colors.white, fontSize: 12)), + ], + ), + ], + ), + ), + ], + ), + InkWell( + onTap: () { + routeToPage(context, const CreatorProfileDetails()); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Row( + children: [ + const Text("Creator Profile", style: TextStyle(color: Colors.white, fontSize: 16)), + const Spacer(), + Icon(Icons.arrow_forward_ios, color: Colors.grey.shade400), + ], + ), + ), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Row( + children: [ + const Text("Payout History", style: TextStyle(color: Colors.white, fontSize: 16)), + const Spacer(), + Icon(Icons.arrow_forward_ios, color: Colors.grey.shade400), + ], + ), + ), + ], + ), + ), + ); + }), + ); + } +} diff --git a/app/lib/pages/settings/creator_profile/creator_profile.dart b/app/lib/pages/settings/creator_profile/creator_profile_details.dart similarity index 96% rename from app/lib/pages/settings/creator_profile/creator_profile.dart rename to app/lib/pages/settings/creator_profile/creator_profile_details.dart index 6bba1edf5..2df5f95d9 100644 --- a/app/lib/pages/settings/creator_profile/creator_profile.dart +++ b/app/lib/pages/settings/creator_profile/creator_profile_details.dart @@ -4,22 +4,8 @@ import 'package:friend_private/utils/other/text_formatters.dart'; import 'package:friend_private/utils/other/validators.dart'; import 'package:provider/provider.dart'; -class CreatorProfileWrapper extends StatelessWidget { - const CreatorProfileWrapper({super.key}); - - @override - Widget build(BuildContext context) { - return ListenableProvider( - create: (_) => CreatorProfileProvider()..getCreatorProfileDetails(), - builder: (context, child) { - return const CreatorProfile(); - }, - ); - } -} - -class CreatorProfile extends StatelessWidget { - const CreatorProfile({super.key}); +class CreatorProfileDetails extends StatelessWidget { + const CreatorProfileDetails({super.key}); @override Widget build(BuildContext context) { @@ -29,7 +15,7 @@ class CreatorProfile extends StatelessWidget { appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.primary, elevation: 0, - title: const Text('Creator Profile'), + title: const Text('Creator Profile Details'), centerTitle: false, leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new), diff --git a/app/lib/pages/settings/creator_profile/creator_profile_provider.dart b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart index 35369c4f8..aff83be06 100644 --- a/app/lib/pages/settings/creator_profile/creator_profile_provider.dart +++ b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart @@ -15,6 +15,11 @@ class CreatorProfileProvider extends ChangeNotifier { bool isLoading = false; bool profileExists = false; + int totalUsage = 0; + double totalEarnings = 0.0; + int publishedApps = 0; + int totalUsers = 0; + void setIsLoading(bool value) { isLoading = value; notifyListeners(); @@ -32,7 +37,23 @@ class CreatorProfileProvider extends ChangeNotifier { notifyListeners(); } + Future getCreatorStats() async { + setIsLoading(true); + var res = await getCreatorStatsServer(); + if (res != null) { + totalUsage = res.usageCount; + totalEarnings = res.moneyMade; + publishedApps = res.appsCount; + totalUsers = res.activeUsers; + setIsLoading(false); + } else { + AppSnackbar.showSnackbarError('Failed to fetch your Apps stats'); + } + notifyListeners(); + } + Future getCreatorProfileDetails() async { + setIsLoading(true); var res = await getCreatorProfile(); if (res != null) { if (res.isEmpty()) { @@ -41,13 +62,15 @@ class CreatorProfileProvider extends ChangeNotifier { profileExists = true; creatorNameController.text = res.creatorName; creatorEmailController.text = res.creatorEmail; - paypalEmailController.text = res.paypalEmail; - paypalMeLinkController.text = res.paypalMeLink ?? ''; + paypalEmailController.text = res.paypalDetails.email; + paypalMeLinkController.text = res.paypalDetails.paypalMeLink ?? ''; } showSubmitButton = false; + setIsLoading(false); } else { AppSnackbar.showSnackbarError('Failed to fetch your Creator Profile details'); } + notifyListeners(); } Future updateDetails() async { @@ -77,8 +100,10 @@ class CreatorProfileProvider extends ChangeNotifier { var profile = CreatorProfile( creatorName: creatorNameController.text, creatorEmail: creatorEmailController.text, - paypalEmail: paypalEmailController.text, - paypalMeLink: paypalMeLinkController.text, + paypalDetails: PayPalDetails( + email: paypalEmailController.text, + paypalMeLink: paypalMeLinkController.text, + ), isVerified: false, ); var res = await saveCreatorProfile(profile); diff --git a/app/lib/pages/settings/page.dart b/app/lib/pages/settings/page.dart index fc905d48f..ef40848c6 100644 --- a/app/lib/pages/settings/page.dart +++ b/app/lib/pages/settings/page.dart @@ -3,7 +3,7 @@ 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/settings/about.dart'; -import 'package:friend_private/pages/settings/creator_profile/creator_profile.dart'; +import 'package:friend_private/pages/settings/creator_profile/creator_dashboard.dart'; import 'package:friend_private/pages/settings/developer.dart'; import 'package:friend_private/pages/settings/profile.dart'; import 'package:friend_private/pages/settings/widgets.dart'; @@ -83,8 +83,8 @@ class _SettingsPageState extends State { ), const SizedBox(height: 8), getItemAddOn2( - 'Creator Profile', - () => routeToPage(context, const CreatorProfileWrapper()), + 'Creator Dashboard', + () => routeToPage(context, const CreatorDashboard()), icon: Icons.person, ), const SizedBox(height: 20), From 166b93b408449983d280c99f3a27052037320723 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:31:26 +0530 Subject: [PATCH 11/19] update creator details model --- backend/database/apps.py | 6 ++++++ backend/models/users.py | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/backend/database/apps.py b/backend/database/apps.py index 62e52b17e..43519a7e5 100644 --- a/backend/database/apps.py +++ b/backend/database/apps.py @@ -30,6 +30,12 @@ def migrate_reviews_from_redis_to_firestore(): new_app_ref.set(review) +def get_user_public_apps_db(uid: str) -> List: + filters = [FieldFilter('uid', '==', uid), FieldFilter('deleted', '==', False), FieldFilter('private', '==', False)] + user_apps = db.collection('plugins_data').where(filter=BaseCompositeFilter('AND', filters)).stream() + return [doc.to_dict() for doc in user_apps] + + def get_app_by_id_db(app_id: str): app_ref = db.collection('plugins_data').document(app_id) doc = app_ref.get() diff --git a/backend/models/users.py b/backend/models/users.py index 16ecd2822..b0ece436b 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -3,6 +3,7 @@ from pydantic import BaseModel from typing import Optional + class WebhookType(str, Enum): audio_bytes = 'audio_bytes' audio_bytes_websocket = 'audio_bytes_websocket' @@ -11,8 +12,12 @@ class WebhookType(str, Enum): day_summary = 'day_summary' +class PayPalDetails(BaseModel): + paypal_email: str + paypal_me_link: Optional[str] = None + + class CreatorProfileRequest(BaseModel): creator_name: str creator_email: str - paypal_email: str - paypal_me_link: Optional[str] \ No newline at end of file + paypal_details: PayPalDetails From 99144c464d027405a9cc835105ec77b87430b34d Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:32:09 +0530 Subject: [PATCH 12/19] assets and schema --- app/assets/images/ic_apps.svg | 7 +++ app/assets/images/ic_chart2.svg | 7 +++ app/assets/images/ic_money.svg | 8 ++++ app/assets/images/ic_users.svg | 7 +++ app/lib/backend/http/api/users.dart | 20 ++++++++ app/lib/backend/schema/profile.dart | 72 ++++++++++++++++++++++++----- app/lib/gen/assets.gen.dart | 16 +++++++ app/lib/main.dart | 2 + 8 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 app/assets/images/ic_apps.svg create mode 100644 app/assets/images/ic_chart2.svg create mode 100644 app/assets/images/ic_money.svg create mode 100644 app/assets/images/ic_users.svg diff --git a/app/assets/images/ic_apps.svg b/app/assets/images/ic_apps.svg new file mode 100644 index 000000000..318db96a4 --- /dev/null +++ b/app/assets/images/ic_apps.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/assets/images/ic_chart2.svg b/app/assets/images/ic_chart2.svg new file mode 100644 index 000000000..a295e715e --- /dev/null +++ b/app/assets/images/ic_chart2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/assets/images/ic_money.svg b/app/assets/images/ic_money.svg new file mode 100644 index 000000000..0dfaca64e --- /dev/null +++ b/app/assets/images/ic_money.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/images/ic_users.svg b/app/assets/images/ic_users.svg new file mode 100644 index 000000000..c8b7aad25 --- /dev/null +++ b/app/assets/images/ic_users.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/lib/backend/http/api/users.dart b/app/lib/backend/http/api/users.dart index 0556f1ad5..648005828 100644 --- a/app/lib/backend/http/api/users.dart +++ b/app/lib/backend/http/api/users.dart @@ -345,3 +345,23 @@ Future updateCreatorProfileServer(String? name, String? email, String? pay return false; } } + +Future getCreatorStatsServer() async { + try { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/users/creator-stats', + headers: {}, + method: 'GET', + body: '', + ); + if (response == null) return null; + debugPrint('getCreatorStatsServer response: ${response.body}'); + if (response.statusCode == 200) { + return CreatorStats.fromJson(jsonDecode(response.body)); + } + return null; + } catch (e) { + debugPrint('getCreatorStatsServer error: $e'); + return null; + } +} diff --git a/app/lib/backend/schema/profile.dart b/app/lib/backend/schema/profile.dart index 9812a8872..f0e53eb43 100644 --- a/app/lib/backend/schema/profile.dart +++ b/app/lib/backend/schema/profile.dart @@ -1,15 +1,37 @@ +class PayPalDetails { + final String email; + final String? paypalMeLink; + + PayPalDetails({ + required this.email, + this.paypalMeLink, + }); + + factory PayPalDetails.fromJson(Map json) { + return PayPalDetails( + email: json['paypal_email'], + paypalMeLink: json['paypal_me_link'] ?? '', + ); + } + + Map toJson() { + return { + 'paypal_email': email, + 'paypal_me_link': paypalMeLink ?? '', + }; + } +} + class CreatorProfile { final String creatorName; final String creatorEmail; - final String paypalEmail; - final String? paypalMeLink; + final PayPalDetails paypalDetails; final bool? isVerified; CreatorProfile({ required this.creatorName, required this.creatorEmail, - required this.paypalEmail, - this.paypalMeLink, + required this.paypalDetails, this.isVerified, }); @@ -17,8 +39,7 @@ class CreatorProfile { return CreatorProfile( creatorName: json['creator_name'], creatorEmail: json['creator_email'], - paypalEmail: json['paypal_email'], - paypalMeLink: json['paypal_me_link'] ?? '', + paypalDetails: PayPalDetails.fromJson(json['paypal_details']), isVerified: json['is_verified'] ?? false, ); } @@ -27,23 +48,52 @@ class CreatorProfile { return { 'creator_name': creatorName, 'creator_email': creatorEmail, - 'paypal_email': paypalEmail, - 'paypal_me_link': paypalMeLink ?? '', + 'paypal_details': paypalDetails.toJson(), 'is_verified': isVerified ?? false, }; } bool isEmpty() { - return creatorName.isEmpty && creatorEmail.isEmpty && paypalEmail.isEmpty; + return creatorName.isEmpty && creatorEmail.isEmpty && paypalDetails.email.isEmpty; } static CreatorProfile empty() { return CreatorProfile( creatorName: '', creatorEmail: '', - paypalEmail: '', - paypalMeLink: '', + paypalDetails: PayPalDetails(email: ''), isVerified: false, ); } } + +class CreatorStats { + final int usageCount; + final double moneyMade; + final int appsCount; + final int activeUsers; + + CreatorStats({ + required this.usageCount, + required this.moneyMade, + required this.appsCount, + required this.activeUsers, + }); + + factory CreatorStats.fromJson(Map json) { + var usageCount = json['usage_count'] as Map; + var totalUsage = usageCount.values.fold(0, (prev, element) => (prev + element).toInt()); + + var moneyMade = json['money_made'] as Map; + var totalMoneyMade = moneyMade.values.fold(0.0, (prev, element) => (prev + element)); + var activeUsers = json['active_users'] as Map; + var totalActiveUsers = activeUsers.values.fold(0, (prev, element) => (prev + element).toInt()); + + return CreatorStats( + usageCount: totalUsage, + moneyMade: totalMoneyMade, + appsCount: json['apps_count'].length, + activeUsers: totalActiveUsers, + ); + } +} diff --git a/app/lib/gen/assets.gen.dart b/app/lib/gen/assets.gen.dart index 5fbb42468..ec98e838c 100644 --- a/app/lib/gen/assets.gen.dart +++ b/app/lib/gen/assets.gen.dart @@ -86,12 +86,24 @@ class $AssetsImagesGen { AssetGenImage get herologo => const AssetGenImage('assets/images/herologo.png'); + /// File path: assets/images/ic_apps.svg + String get icApps => 'assets/images/ic_apps.svg'; + /// File path: assets/images/ic_chart.svg String get icChart => 'assets/images/ic_chart.svg'; + /// File path: assets/images/ic_chart2.svg + String get icChart2 => 'assets/images/ic_chart2.svg'; + /// File path: assets/images/ic_dollar.svg String get icDollar => 'assets/images/ic_dollar.svg'; + /// File path: assets/images/ic_money.svg + String get icMoney => 'assets/images/ic_money.svg'; + + /// File path: assets/images/ic_users.svg + String get icUsers => 'assets/images/ic_users.svg'; + /// File path: assets/images/instruction_1.png AssetGenImage get instruction1 => const AssetGenImage('assets/images/instruction_1.png'); @@ -137,8 +149,12 @@ class $AssetsImagesGen { blob, emotionalFeedback1, herologo, + icApps, icChart, + icChart2, icDollar, + icMoney, + icUsers, instruction1, instruction2, instruction3, diff --git a/app/lib/main.dart b/app/lib/main.dart index 6446adbdf..c7f0b7d43 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -22,6 +22,7 @@ import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/memory_detail/memory_detail_provider.dart'; import 'package:friend_private/pages/onboarding/wrapper.dart'; +import 'package:friend_private/pages/settings/creator_profile/creator_profile_provider.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/providers/auth_provider.dart'; import 'package:friend_private/providers/calendar_provider.dart'; @@ -203,6 +204,7 @@ class _MyAppState extends State with WidgetsBindingObserver { update: (BuildContext context, value, AddAppProvider? previous) => (previous?..setAppProvider(value)) ?? AddAppProvider(), ), + ChangeNotifierProvider(create: (context) => CreatorProfileProvider()), ], builder: (context, child) { return WithForegroundTask( From 50b35d8b0c63f32a745d844a7e04b6971f0b91cd Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:38:34 +0530 Subject: [PATCH 13/19] creator profile and payouts endpoints and models --- backend/database/apps.py | 10 ++++++++++ backend/database/redis_db.py | 2 +- backend/database/users.py | 12 ++++++++++++ backend/models/users.py | 22 +++++++++++++++++++++ backend/routers/users.py | 37 +++++++++++++++++++++++++++++------- backend/utils/user.py | 18 ++++++++++++++++++ 6 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 backend/utils/user.py diff --git a/backend/database/apps.py b/backend/database/apps.py index 43519a7e5..8bce95808 100644 --- a/backend/database/apps.py +++ b/backend/database/apps.py @@ -36,6 +36,16 @@ def get_user_public_apps_db(uid: str) -> List: return [doc.to_dict() for doc in user_apps] +def batch_update_creator_profile_for_apps_db(uid: str, author: str, email: str): + filters = [FieldFilter('uid', '==', uid), FieldFilter('deleted', '==', False)] + user_apps = db.collection('plugins_data').where(filter=BaseCompositeFilter('AND', filters)).stream() + batch = db.batch() + for doc in user_apps: + app_ref = db.collection('plugins_data').document(doc.id) + batch.update(app_ref, {'author': author, 'email': email}) + batch.commit() + + def get_app_by_id_db(app_id: str): app_ref = db.collection('plugins_data').document(app_id) doc = app_ref.get() diff --git a/backend/database/redis_db.py b/backend/database/redis_db.py index 2229615c6..4dedf56f2 100644 --- a/backend/database/redis_db.py +++ b/backend/database/redis_db.py @@ -94,7 +94,7 @@ def get_multiple_apps_usage_count_cache(app_ids: list) -> dict: if counts is None: return {} return { - app_id: int(count) if count else 0 + app_id: eval(count) if count else 0 for app_id, count in zip(app_ids, counts) } diff --git a/backend/database/users.py b/backend/database/users.py index dc7f53001..9166a69c7 100644 --- a/backend/database/users.py +++ b/backend/database/users.py @@ -93,6 +93,18 @@ def set_user_creator_profile_db(uid: str, data: dict): user_ref.update({'creator_profile': data}) +def create_manual_payment_db(uid: str, data: dict): + manual_payment_ref = db.collection('users').document(uid).collection('payments') + manual_payment_ref.document().set(data) + return data + + +def get_all_user_payouts_db(uid: str): + payments_ref = db.collection('users').document(uid).collection('payments') + payments = payments_ref.stream() + return [payment.to_dict() for payment in payments] + + # ************************************** # ************* Analytics ************** # ************************************** diff --git a/backend/models/users.py b/backend/models/users.py index b0ece436b..84c3de503 100644 --- a/backend/models/users.py +++ b/backend/models/users.py @@ -1,3 +1,4 @@ +from datetime import datetime from enum import Enum from pydantic import BaseModel @@ -21,3 +22,24 @@ class CreatorProfileRequest(BaseModel): creator_name: str creator_email: str paypal_details: PayPalDetails + + +class Amount(BaseModel): + value: str + currency_code: str + + +class Payee(BaseModel): + email: str + uid: str + payment_method: str + + +class ManualPaymentRequest(BaseModel): + amount: Amount + payment_method: str + payment_mode: str + payment_status: str + payee: Payee + description: Optional[str] = None + supplementary_data: Optional[dict] = None diff --git a/backend/routers/users.py b/backend/routers/users.py index 6fd08e389..d6cd01e1b 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -1,23 +1,26 @@ +import os import threading import uuid from typing import List -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Header -from database.apps import get_user_public_apps_db +from database.apps import get_user_public_apps_db, batch_update_creator_profile_for_apps_db from database.memories import get_in_progress_memory, get_memory from database.redis_db import cache_user_geolocation, set_user_webhook_db, get_user_webhook_db, disable_user_webhook_db, \ enable_user_webhook_db, user_webhook_status_db from database.users import * from models.memory import Geolocation, Memory from models.other import Person, CreatePerson -from models.users import WebhookType, CreatorProfileRequest +from models.users import WebhookType, CreatorProfileRequest, ManualPaymentRequest from utils.apps import get_multiple_apps_usage_count, get_multiple_apps_money_made_amount, \ get_multiple_apps_enabled_count from utils.llm import followup_question_prompt from utils.other import endpoints as auth from utils.other.storage import delete_all_memory_recordings, get_user_person_speech_samples, \ delete_user_person_speech_samples +from utils.user import get_user_creator_profile, create_user_creator_profile, update_user_creator_profile, \ + update_creator_details_for_user_apps from utils.webhooks import webhook_first_time_setup router = APIRouter() @@ -50,21 +53,22 @@ def set_creator_profile(data: CreatorProfileRequest, uid: str = Depends(auth.get data = data.dict() data['created_at'] = datetime.now(timezone.utc) data['is_verified'] = False - set_user_creator_profile_db(uid, data) + create_user_creator_profile(uid, data) return {'status': 'ok'} @router.get('/v1/users/creator-profile', tags=['v1']) def get_creator_profile(uid: str = Depends(auth.get_current_user_uid)): - return get_user_creator_profile_db(uid) + return get_user_creator_profile(uid) @router.patch('/v1/users/creator-profile', tags=['v1']) def update_creator_profile(data: dict, uid: str = Depends(auth.get_current_user_uid)): - current_data = get_user_creator_profile_db(uid) + current_data = get_user_creator_profile(uid) current_data.update(data) current_data['updated_at'] = datetime.now(timezone.utc) - set_user_creator_profile_db(uid, current_data) + update_user_creator_profile(uid, current_data) + update_creator_details_for_user_apps(uid, current_data) return {'status': 'ok'} @@ -83,6 +87,11 @@ def get_creator_stats(uid: str = Depends(auth.get_current_user_uid)): } +@router.get('/v1/users/payout-history', tags=['v1']) +def get_creator_stats(uid: str = Depends(auth.get_current_user_uid)): + return get_all_user_payouts_db(uid) + + # *********************************************** # ************* DEVELOPER WEBHOOKS ************** # *********************************************** @@ -271,3 +280,17 @@ def set_chat_message_analytics( ): set_chat_message_rating_score(uid, message_id, value) return {'status': 'ok'} + + +# **************************************** +# ************ TEAM ENDPOINTS ************ +# **************************************** + +@router.post('/v1/users/{uid}/manual-payment', tags=['v1']) +def create_manual_payment(data: ManualPaymentRequest, uid: str,secret_key: str = Header(...)): + if secret_key != os.getenv('ADMIN_KEY'): + raise HTTPException(status_code=403, detail='You are not authorized to perform this action') + data = data.dict() + data['payment_date'] = datetime.now(timezone.utc) + create_manual_payment_db(uid, data) + return {'status': 'ok'} diff --git a/backend/utils/user.py b/backend/utils/user.py new file mode 100644 index 000000000..81f005ee4 --- /dev/null +++ b/backend/utils/user.py @@ -0,0 +1,18 @@ +from database.apps import batch_update_creator_profile_for_apps_db +from database.users import get_user_creator_profile_db, set_user_creator_profile_db + + +def get_user_creator_profile(uid: str): + return get_user_creator_profile_db(uid) + + +def update_user_creator_profile(uid: str, data: dict): + return set_user_creator_profile_db(uid, data) + + +def update_creator_details_for_user_apps(uid: str, data: dict): + return batch_update_creator_profile_for_apps_db(uid, data['creator_name'], data['creator_email']) + + +def create_user_creator_profile(uid: str, data: dict): + return set_user_creator_profile_db(uid, data) From 9b3c5fd898f9f4380465a54a26f6ea8b41019787 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:40:06 +0530 Subject: [PATCH 14/19] payout and dashboard ui and improvements and logic --- app/lib/backend/http/api/apps.dart | 4 +- app/lib/backend/http/api/users.dart | 26 +++- app/lib/backend/schema/profile.dart | 69 ++++++++++ .../creator_profile/creator_dashboard.dart | 35 +++--- .../creator_profile_provider.dart | 34 ++++- .../creator_profile/payout_history.dart | 119 ++++++++++++++++++ 6 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 app/lib/pages/settings/creator_profile/payout_history.dart diff --git a/app/lib/backend/http/api/apps.dart b/app/lib/backend/http/api/apps.dart index 07db69a92..2b69bfbd3 100644 --- a/app/lib/backend/http/api/apps.dart +++ b/app/lib/backend/http/api/apps.dart @@ -143,10 +143,8 @@ Future isAppSetupCompleted(String? url) async { headers: {}, body: '', ); - var data; try { - data = jsonDecode(response?.body ?? '{}'); - print(data); + var data = jsonDecode(response?.body ?? '{}'); return data['is_setup_completed'] ?? false; } on FormatException catch (e) { debugPrint('Response not a valid json: $e'); diff --git a/app/lib/backend/http/api/users.dart b/app/lib/backend/http/api/users.dart index 7547e29b9..fa36e9334 100644 --- a/app/lib/backend/http/api/users.dart +++ b/app/lib/backend/http/api/users.dart @@ -333,8 +333,10 @@ Future updateCreatorProfileServer(String? name, String? email, String? pay body: jsonEncode({ 'creator_name': name, 'creator_email': email, - 'paypal_email': paypalEmail, - 'paypal_me_link': paypalLink, + 'paypal_details': { + 'paypal_email': paypalEmail, + 'paypal_me_link': paypalLink, + }, }), ); if (response == null) return false; @@ -365,3 +367,23 @@ Future getCreatorStatsServer() async { return null; } } + +Future getPayoutHistoryServer() async { + try { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/users/payout-history', + headers: {}, + method: 'GET', + body: '', + ); + if (response == null) return null; + debugPrint('getPayoutHistoryServer response: ${response.body}'); + if (response.statusCode == 200) { + return PayoutTransaction.fromJsonList(jsonDecode(response.body)); + } + return null; + } catch (e) { + debugPrint('getPayoutHistoryServer error: $e'); + return null; + } +} diff --git a/app/lib/backend/schema/profile.dart b/app/lib/backend/schema/profile.dart index f0e53eb43..897bed68b 100644 --- a/app/lib/backend/schema/profile.dart +++ b/app/lib/backend/schema/profile.dart @@ -97,3 +97,72 @@ class CreatorStats { ); } } + +class PayoutTransaction { + final String amount; + final String currency; + final DateTime date; + final String paymentStatus; + final String payoutMethod; + + PayoutTransaction({ + required this.amount, + required this.date, + required this.paymentStatus, + required this.payoutMethod, + required this.currency, + }); + + factory PayoutTransaction.fromJson(Map json) { + return PayoutTransaction( + amount: json['amount']['value'], + currency: json['amount']['currency_code'], + date: DateTime.parse(json['payment_date']).toLocal(), + paymentStatus: json['payment_status'], + payoutMethod: json['payee']['payment_method'], + ); + } + + PayoutTransaction.empty() + : amount = '', + currency = '', + date = DateTime.now(), + paymentStatus = '', + payoutMethod = ''; + + bool isPending() { + return paymentStatus == 'pending'; + } + + bool isSuccessful() { + return paymentStatus == 'successful'; + } + + bool isFailed() { + return paymentStatus == 'failed'; + } + + String paymentStatusText() { + if (isPending()) { + return 'Pending'; + } else if (isSuccessful()) { + return 'Successful'; + } else if (isFailed()) { + return 'Failed'; + } else { + return 'Unknown'; + } + } + + String payoutMethodText() { + if (payoutMethod == 'paypal') { + return 'PayPal'; + } else { + return 'Unknown'; + } + } + + static List fromJsonList(List jsonList) { + return jsonList.map((e) => PayoutTransaction.fromJson(e)).toList(); + } +} diff --git a/app/lib/pages/settings/creator_profile/creator_dashboard.dart b/app/lib/pages/settings/creator_profile/creator_dashboard.dart index 372861a71..7ad1b75a5 100644 --- a/app/lib/pages/settings/creator_profile/creator_dashboard.dart +++ b/app/lib/pages/settings/creator_profile/creator_dashboard.dart @@ -7,6 +7,8 @@ import 'package:friend_private/utils/other/temp.dart'; import 'package:provider/provider.dart'; import 'package:skeletonizer/skeletonizer.dart'; +import 'payout_history.dart'; + class CreatorDashboard extends StatefulWidget { const CreatorDashboard({super.key}); @@ -168,20 +170,25 @@ class _CreatorDashboardState extends State { ), ), ), - Container( - width: double.infinity, - padding: const EdgeInsets.all(16.0), - margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), - decoration: BoxDecoration( - color: Colors.grey.shade900, - borderRadius: BorderRadius.circular(16.0), - ), - child: Row( - children: [ - const Text("Payout History", style: TextStyle(color: Colors.white, fontSize: 16)), - const Spacer(), - Icon(Icons.arrow_forward_ios, color: Colors.grey.shade400), - ], + InkWell( + onTap: () { + routeToPage(context, const PayoutHistory()); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Row( + children: [ + const Text("Payout History", style: TextStyle(color: Colors.white, fontSize: 16)), + const Spacer(), + Icon(Icons.arrow_forward_ios, color: Colors.grey.shade400), + ], + ), ), ), ], diff --git a/app/lib/pages/settings/creator_profile/creator_profile_provider.dart b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart index aff83be06..723ede38d 100644 --- a/app/lib/pages/settings/creator_profile/creator_profile_provider.dart +++ b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart @@ -20,6 +20,15 @@ class CreatorProfileProvider extends ChangeNotifier { int publishedApps = 0; int totalUsers = 0; + List dummyPayoutHistory = List.generate(6, (index) => PayoutTransaction.empty()); + + List payoutHistory = []; + + CreatorProfileProvider() { + payoutHistory = dummyPayoutHistory; + getCreatorProfile(); + } + void setIsLoading(bool value) { isLoading = value; notifyListeners(); @@ -52,12 +61,24 @@ class CreatorProfileProvider extends ChangeNotifier { notifyListeners(); } + Future getPayoutHistory() async { + setIsLoading(true); + var res = await getPayoutHistoryServer(); + if (res != null) { + payoutHistory = res; + setIsLoading(false); + } else { + AppSnackbar.showSnackbarError('Failed to fetch your Payout History'); + } + notifyListeners(); + } + Future getCreatorProfileDetails() async { setIsLoading(true); var res = await getCreatorProfile(); if (res != null) { if (res.isEmpty()) { - AppSnackbar.showSnackbarInfo('Looks like you have not created your Creator Profile yet'); + AppSnackbar.showSnackbarInfo('Please complete your profile to receive payments.'); } else { profileExists = true; creatorNameController.text = res.creatorName; @@ -68,7 +89,7 @@ class CreatorProfileProvider extends ChangeNotifier { showSubmitButton = false; setIsLoading(false); } else { - AppSnackbar.showSnackbarError('Failed to fetch your Creator Profile details'); + AppSnackbar.showSnackbarError('Failed to fetch your creator profile details'); } notifyListeners(); } @@ -83,11 +104,12 @@ class CreatorProfileProvider extends ChangeNotifier { paypalMeLinkController.text, ); if (res) { - AppSnackbar.showSnackbarSuccess('Creator Profile updated successfully'); + AppSnackbar.showSnackbarSuccess('Creator profile details updated successfully'); } else { - AppSnackbar.showSnackbarError('Failed to update Creator Profile'); + AppSnackbar.showSnackbarError('Failed to update your creator profile details'); } showSubmitButton = false; + profileExists = true; setIsLoading(false); } else { AppSnackbar.showSnackbarError('Please fill all the fields correctly'); @@ -109,9 +131,9 @@ class CreatorProfileProvider extends ChangeNotifier { var res = await saveCreatorProfile(profile); if (res) { profileExists = true; - AppSnackbar.showSnackbarSuccess('Creator Profile saved successfully'); + AppSnackbar.showSnackbarSuccess('Creator profile details saved successfully'); } else { - AppSnackbar.showSnackbarError('Failed to update Creator Profile'); + AppSnackbar.showSnackbarError('Failed to update your creator profile details'); } showSubmitButton = false; setIsLoading(false); diff --git a/app/lib/pages/settings/creator_profile/payout_history.dart b/app/lib/pages/settings/creator_profile/payout_history.dart new file mode 100644 index 000000000..24f37da7b --- /dev/null +++ b/app/lib/pages/settings/creator_profile/payout_history.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/utils/other/temp.dart'; +import 'package:provider/provider.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +import 'creator_profile_provider.dart'; + +class PayoutHistory extends StatefulWidget { + const PayoutHistory({super.key}); + + @override + State createState() => _PayoutHistoryState(); +} + +class _PayoutHistoryState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await Provider.of(context, listen: false).getPayoutHistory(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + elevation: 0, + title: const Text('Payout History'), + centerTitle: false, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + body: Consumer(builder: (context, provider, child) { + return Skeletonizer( + enabled: provider.isLoading, + child: ListView.builder( + itemBuilder: (ctx, idx) { + return Container( + padding: const EdgeInsets.all(16.0), + margin: const EdgeInsets.only(left: 8.0, right: 8.0, top: 12, bottom: 6), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(16.0), + ), + child: Column( + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${provider.payoutHistory[idx].amount} ${provider.payoutHistory[idx].currency}", + style: const TextStyle(color: Colors.white, fontSize: 20)), + const SizedBox( + height: 4, + ), + Text(dateTimeFormat('MMM d, h:mm a', provider.payoutHistory[idx].date), + style: const TextStyle(color: Colors.white, fontSize: 12)), + ], + ), + const Spacer(), + getStatusChip(provider.payoutHistory[idx].paymentStatus), + ], + ), + const SizedBox( + height: 6, + ), + Divider( + color: Colors.grey.shade700, + thickness: 1, + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + const Text('Payment Method', style: TextStyle(color: Colors.white)), + const Spacer(), + Text(provider.payoutHistory[idx].payoutMethodText(), + style: const TextStyle(color: Colors.white)), + ], + ), + ], + ), + ); + }, + itemCount: provider.payoutHistory.length, + ), + ); + }), + ); + } + + Widget getStatusChip(String status) { + if (status == 'pending') { + return const Chip( + label: Text("Pending", style: TextStyle(color: Colors.white)), + ); + } else if (status == 'failed') { + return Chip( + backgroundColor: Colors.red.withOpacity(0.4), + label: const Text("Failed", style: TextStyle(color: Colors.red)), + ); + } else { + return Chip( + backgroundColor: const Color(0xFF4CAF50).withOpacity(0.4), + label: const Text("Successful", style: TextStyle(color: Color.fromARGB(255, 40, 231, 46))), + ); + } + } +} From 92d2ccdeccc21b483d45f9e37113c3302786585c Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:40:25 +0530 Subject: [PATCH 15/19] creator profile check when submit app clicked --- app/lib/pages/apps/explore_install_page.dart | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 75ff4a9f5..d31916b24 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -5,12 +5,16 @@ import 'package:friend_private/pages/apps/providers/add_app_provider.dart'; import 'package:friend_private/pages/apps/widgets/app_section_card.dart'; import 'package:friend_private/pages/apps/widgets/filter_sheet.dart'; import 'package:friend_private/pages/apps/list_item.dart'; +import 'package:friend_private/pages/settings/creator_profile/creator_profile_provider.dart'; import 'package:friend_private/providers/app_provider.dart'; import 'package:friend_private/providers/home_provider.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:provider/provider.dart'; +import '../settings/creator_profile/creator_profile_details.dart'; + String filterValueToString(dynamic value) { if (value.runtimeType == String) { return value; @@ -191,7 +195,24 @@ class _ExploreInstallPageState extends State with AutomaticK child: GestureDetector( onTap: () { MixpanelManager().pageOpened('Submit App'); - routeToPage(context, const AddAppPage()); + if (context.read().profileExists) { + routeToPage(context, const AddAppPage()); + } else { + showDialog( + context: context, + builder: (c) => getDialog( + context, + () => Navigator.pop(context), + () async { + Navigator.pop(context); + routeToPage(context, const CreatorProfileDetails()); + }, + 'App Creator Program', + "\nJoin Omi App Creator Program to submit apps and receive payments", + okButtonText: 'Join Now', + ), + ); + } }, child: Container( padding: const EdgeInsets.all(12.0), From f3fe25f01d0d0b3ea5e1cf0b77299e6b2ff7b1ff Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:40:39 +0530 Subject: [PATCH 16/19] mounted check for usage and money --- app/lib/pages/apps/app_detail/app_detail.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/lib/pages/apps/app_detail/app_detail.dart b/app/lib/pages/apps/app_detail/app_detail.dart index f91ea099a..b1065ed8b 100644 --- a/app/lib/pages/apps/app_detail/app_detail.dart +++ b/app/lib/pages/apps/app_detail/app_detail.dart @@ -67,13 +67,15 @@ class _AppDetailPageState extends State { if (!widget.app.private) { setAnalyticsLoading(true); var app = await context.read().getAppDetails(widget.app.id); - setState(() { - if (app != null) { - moneyMade = app.moneyMade ?? 0.0; - usageCount = app.usageCount ?? 0; - } - }); - setAnalyticsLoading(false); + if (mounted) { + setState(() { + if (app != null) { + moneyMade = app.moneyMade ?? 0.0; + usageCount = app.usageCount ?? 0; + } + }); + setAnalyticsLoading(false); + } } context.read().checkIsAppOwner(widget.app.uid); context.read().setIsAppPublicToggled(!widget.app.private); From c402528306401e4e72ff0524f170e4252947cfce Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:41:08 +0530 Subject: [PATCH 17/19] update v2 endpoint to submit app --- backend/routers/apps.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/backend/routers/apps.py b/backend/routers/apps.py index c6dd185fe..4f0ea37e0 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -21,6 +21,7 @@ from utils.other import endpoints as auth from models.app import App, SubmitAppRequest from utils.other.storage import upload_plugin_logo, delete_plugin_logo +from utils.user import get_user_creator_profile router = APIRouter() @@ -79,6 +80,12 @@ def submit_app_v2(app_data: str = Form(...), file: UploadFile = File(...), uid=D data = validated_data.dict() data['name'] = data['name'].strip() data['id'] = str(ULID()) + data['uid'] = uid + creator_profile = get_user_creator_profile(uid) + if not creator_profile: + raise HTTPException(status_code=403, detail='You need to create a creator profile first') + data['author'] = creator_profile['creator_name'] + data['email'] = creator_profile['creator_email'] if external_integration := data.get('external_integration'): if external_integration.get('triggers_on') is None: raise HTTPException(status_code=422, detail='Triggers on is required') @@ -90,12 +97,12 @@ def submit_app_v2(app_data: str = Form(...), file: UploadFile = File(...), uid=D external_integration['is_instructions_url'] = True else: external_integration['is_instructions_url'] = False - os.makedirs(f'_temp/plugins', exist_ok=True) - file_path = f"_temp/plugins/{file.filename}" + os.makedirs(f'_temp/apps', exist_ok=True) + file_path = f"_temp/apps/{file.filename}" with open(file_path, 'wb') as f: f.write(file.file.read()) - imgUrl = upload_plugin_logo(file_path, data['id']) - data['image'] = imgUrl + img_url = upload_plugin_logo(file_path, data['id']) + data['image'] = img_url data['created_at'] = datetime.now(timezone.utc) add_app_to_db(data) return {'status': 'ok'} @@ -105,13 +112,13 @@ def submit_app_v2(app_data: str = Form(...), file: UploadFile = File(...), uid=D def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(None), uid=Depends(auth.get_current_user_uid)): data = json.loads(app_data) - plugin = get_available_app_by_id(app_id, uid) - if not plugin: + app = get_available_app_by_id(app_id, uid) + if not app: raise HTTPException(status_code=404, detail='App not found') - if plugin['uid'] != uid: + if app['uid'] != uid: raise HTTPException(status_code=403, detail='You are not authorized to perform this action') if file: - delete_plugin_logo(plugin['image']) + delete_plugin_logo(app['image']) os.makedirs(f'_temp/plugins', exist_ok=True) file_path = f"_temp/plugins/{file.filename}" with open(file_path, 'wb') as f: @@ -120,7 +127,7 @@ def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(N data['image'] = img_url data['updated_at'] = datetime.now(timezone.utc) update_app_in_db(data) - if plugin['approved'] and (plugin['private'] is None or plugin['private'] is False): + if app['approved'] and (app['private'] is None or app['private'] is False): delete_generic_cache('get_public_approved_apps_data') delete_app_cache_by_id(app_id) return {'status': 'ok'} From c060376ce5a422cb1627ae6f96769274c62eb871 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:39:50 +0530 Subject: [PATCH 18/19] remove email and name field --- app/lib/pages/apps/add_app.dart | 2 -- app/lib/pages/apps/providers/add_app_provider.dart | 1 - app/lib/pages/apps/update_app.dart | 2 -- 3 files changed, 5 deletions(-) diff --git a/app/lib/pages/apps/add_app.dart b/app/lib/pages/apps/add_app.dart index e4f20b056..8b30cffd6 100644 --- a/app/lib/pages/apps/add_app.dart +++ b/app/lib/pages/apps/add_app.dart @@ -107,8 +107,6 @@ class _AddAppPageState extends State { appPricing: provider.isPaid ? 'Paid' : 'Free', appNameController: provider.appNameController, appDescriptionController: provider.appDescriptionController, - creatorNameController: provider.creatorNameController, - creatorEmailController: provider.creatorEmailController, categories: provider.categories, setAppCategory: provider.setAppCategory, imageFile: provider.imageFile, diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart index 97568d97e..6df812aff 100644 --- a/app/lib/pages/apps/providers/add_app_provider.dart +++ b/app/lib/pages/apps/providers/add_app_provider.dart @@ -121,7 +121,6 @@ class AddAppProvider extends ChangeNotifier { appNameController.text = app.name.decodeString; appDescriptionController.text = app.description.decodeString; priceController.text = app.price.toString(); - creatorEmailController.text = app.email ?? ''; makeAppPublic = !app.private; selectedCapabilities = app.getCapabilitiesFromIds(capabilities); if (app.externalIntegration != null) { diff --git a/app/lib/pages/apps/update_app.dart b/app/lib/pages/apps/update_app.dart index f767a1994..836a7dfb9 100644 --- a/app/lib/pages/apps/update_app.dart +++ b/app/lib/pages/apps/update_app.dart @@ -109,8 +109,6 @@ class _UpdateAppPageState extends State { imageFile: provider.imageFile, appNameController: provider.appNameController, appDescriptionController: provider.appDescriptionController, - creatorNameController: provider.creatorNameController, - creatorEmailController: provider.creatorEmailController, categories: provider.categories, setAppCategory: provider.setAppCategory, imageUrl: provider.imageUrl, From b0a11d69838fc2c30b83e529d89a840a9da43fe1 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:45:29 +0530 Subject: [PATCH 19/19] add payment stuff to v2 /apps --- backend/models/app.py | 5 ++++- backend/routers/apps.py | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/models/app.py b/backend/models/app.py index dbc17f7ab..041b11af8 100644 --- a/backend/models/app.py +++ b/backend/models/app.py @@ -143,4 +143,7 @@ class SubmitAppRequest(BaseModel): chat_prompt: Optional[str] = None external_integration: Optional[ExternalIntegration] = None deleted: bool = False - proactive_notification: Optional[ProactiveNotification] = None \ No newline at end of file + proactive_notification: Optional[ProactiveNotification] = None + is_paid: bool = False + price: float = 0.0 # cents/100 + payment_plan: Optional[str] = None \ No newline at end of file diff --git a/backend/routers/apps.py b/backend/routers/apps.py index 909bb6608..321bc17e3 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -99,7 +99,7 @@ def submit_app_v2(app_data: str = Form(...), file: UploadFile = File(...), uid=D data['uid'] = uid creator_profile = get_user_creator_profile(uid) if not creator_profile: - raise HTTPException(status_code=403, detail='You need to create a creator profile first') + raise HTTPException(status_code=403, detail='Your creator profile is not set up') data['author'] = creator_profile['creator_name'] data['email'] = creator_profile['creator_email'] if external_integration := data.get('external_integration'): @@ -121,6 +121,11 @@ def submit_app_v2(app_data: str = Form(...), file: UploadFile = File(...), uid=D data['image'] = img_url data['created_at'] = datetime.now(timezone.utc) add_app_to_db(data) + + # payment link + app = App(**data) + upsert_app_payment_link(app.id, app.is_paid, app.price, app.payment_plan) + return {'status': 'ok'} @@ -147,9 +152,9 @@ def update_app(app_id: str, app_data: str = Form(...), file: UploadFile = File(N # payment link upsert_app_payment_link(data.get('id'), data.get('is_paid', False), data.get('price'), data.get('payment_plan'), - previous_price=plugin.get("price", 0)) + previous_price=app.get("price", 0)) - if plugin['approved'] and (plugin['private'] is None or plugin['private'] is False): + if app['approved'] and (app['private'] is None or app['private'] is False): delete_generic_cache('get_public_approved_apps_data') delete_app_cache_by_id(app_id) return {'status': 'ok'}