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/apps.dart b/app/lib/backend/http/api/apps.dart
index 146609d82..c2b207133 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');
@@ -160,7 +158,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/backend/http/api/users.dart b/app/lib/backend/http/api/users.dart
index fc05f14c3..fa36e9334 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';
@@ -280,3 +281,109 @@ Future getHasConversationSummaryRating(String conversationId) async {
return false;
}
}
+
+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(profile.toJson()),
+ );
+ if (response == null) return false;
+ debugPrint('saveCreatorProfile response: ${response.body}');
+ return response.statusCode == 200;
+ } catch (e) {
+ debugPrint('saveCreatorProfile error: $e');
+ return false;
+ }
+}
+
+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({
+ 'creator_name': name,
+ 'creator_email': email,
+ 'paypal_details': {
+ 'paypal_email': paypalEmail,
+ 'paypal_me_link': paypalLink,
+ },
+ }),
+ );
+ if (response == null) return false;
+ debugPrint('updateCreatorProfile response: ${response.body}');
+ return response.statusCode == 200;
+ } catch (e) {
+ debugPrint('updateCreatorProfile error: $e');
+ 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;
+ }
+}
+
+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
new file mode 100644
index 000000000..897bed68b
--- /dev/null
+++ b/app/lib/backend/schema/profile.dart
@@ -0,0 +1,168 @@
+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 PayPalDetails paypalDetails;
+ final bool? isVerified;
+
+ CreatorProfile({
+ required this.creatorName,
+ required this.creatorEmail,
+ required this.paypalDetails,
+ this.isVerified,
+ });
+
+ factory CreatorProfile.fromJson(Map json) {
+ return CreatorProfile(
+ creatorName: json['creator_name'],
+ creatorEmail: json['creator_email'],
+ paypalDetails: PayPalDetails.fromJson(json['paypal_details']),
+ isVerified: json['is_verified'] ?? false,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'creator_name': creatorName,
+ 'creator_email': creatorEmail,
+ 'paypal_details': paypalDetails.toJson(),
+ 'is_verified': isVerified ?? false,
+ };
+ }
+
+ bool isEmpty() {
+ return creatorName.isEmpty && creatorEmail.isEmpty && paypalDetails.email.isEmpty;
+ }
+
+ static CreatorProfile empty() {
+ return CreatorProfile(
+ creatorName: '',
+ creatorEmail: '',
+ 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,
+ );
+ }
+}
+
+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/gen/assets.gen.dart b/app/lib/gen/assets.gen.dart
index 4fbd4f9e4..a50f74ad1 100644
--- a/app/lib/gen/assets.gen.dart
+++ b/app/lib/gen/assets.gen.dart
@@ -97,6 +97,9 @@ 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/herologo_v1.png
AssetGenImage get herologoV1 =>
const AssetGenImage('assets/images/herologo_v1.png');
@@ -112,9 +115,18 @@ class $AssetsImagesGen {
/// 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');
@@ -187,11 +199,15 @@ class $AssetsImagesGen {
blob,
emotionalFeedback1,
herologo,
+ icApps,
herologoV1,
herologoV3,
herologoV4,
icChart,
+ icChart2,
icDollar,
+ icMoney,
+ icUsers,
instruction1,
instruction2,
instruction3,
diff --git a/app/lib/main.dart b/app/lib/main.dart
index b0614cc8a..a83607646 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/conversation_detail/conversation_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(
diff --git a/app/lib/pages/apps/add_app.dart b/app/lib/pages/apps/add_app.dart
index 3ed05cb61..620b5d70a 100644
--- a/app/lib/pages/apps/add_app.dart
+++ b/app/lib/pages/apps/add_app.dart
@@ -109,8 +109,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/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),
diff --git a/app/lib/pages/apps/providers/add_app_provider.dart b/app/lib/pages/apps/providers/add_app_provider.dart
index 9083eac0d..a80ae4b0b 100644
--- a/app/lib/pages/apps/providers/add_app_provider.dart
+++ b/app/lib/pages/apps/providers/add_app_provider.dart
@@ -23,8 +23,6 @@ class AddAppProvider extends ChangeNotifier {
TextEditingController appNameController = TextEditingController();
TextEditingController appDescriptionController = TextEditingController();
- TextEditingController creatorNameController = TextEditingController();
- TextEditingController creatorEmailController = TextEditingController();
TextEditingController chatPromptController = TextEditingController();
TextEditingController conversationPromptController = TextEditingController();
String? appCategory;
@@ -79,8 +77,6 @@ class AddAppProvider extends ChangeNotifier {
if (paymentPlans.isEmpty) {
await getPaymentPlans();
}
- creatorNameController.text = SharedPreferencesUtil().givenName;
- creatorEmailController.text = SharedPreferencesUtil().email;
setIsLoading(false);
}
@@ -127,9 +123,7 @@ class AddAppProvider extends ChangeNotifier {
imageUrl = app.image;
appNameController.text = app.name.decodeString;
appDescriptionController.text = app.description.decodeString;
- creatorNameController.text = app.author.decodeString;
priceController.text = app.price.toString();
- creatorEmailController.text = app.email ?? '';
makeAppPublic = !app.private;
selectedCapabilities = app.getCapabilitiesFromIds(capabilities);
if (app.externalIntegration != null) {
@@ -159,8 +153,6 @@ class AddAppProvider extends ChangeNotifier {
void clear() {
appNameController.clear();
appDescriptionController.clear();
- creatorNameController.clear();
- creatorEmailController.clear();
chatPromptController.clear();
conversationPromptController.clear();
triggerEvent = null;
@@ -243,9 +235,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;
}
@@ -410,8 +399,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,
@@ -471,8 +458,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 a125299b0..113f5e858 100644
--- a/app/lib/pages/apps/update_app.dart
+++ b/app/lib/pages/apps/update_app.dart
@@ -111,8 +111,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 2ee71e650..d0be437eb 100644
--- a/app/lib/pages/apps/widgets/app_metadata_widget.dart
+++ b/app/lib/pages/apps/widgets/app_metadata_widget.dart
@@ -15,8 +15,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;
@@ -31,8 +29,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,
@@ -270,75 +266,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/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..7ad1b75a5
--- /dev/null
+++ b/app/lib/pages/settings/creator_profile/creator_dashboard.dart
@@ -0,0 +1,201 @@
+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';
+
+import 'payout_history.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),
+ ],
+ ),
+ ),
+ ),
+ 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_details.dart b/app/lib/pages/settings/creator_profile/creator_profile_details.dart
new file mode 100644
index 000000000..2df5f95d9
--- /dev/null
+++ b/app/lib/pages/settings/creator_profile/creator_profile_details.dart
@@ -0,0 +1,287 @@
+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 CreatorProfileDetails extends StatelessWidget {
+ const CreatorProfileDetails({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Consumer(builder: (context, provider, child) {
+ return Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ appBar: AppBar(
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ elevation: 0,
+ title: const Text('Creator Profile Details'),
+ centerTitle: false,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back_ios_new),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ ),
+ 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),
+ ),
+ 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: 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: 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',
+ ),
+ ),
+ ),
+ ],
+ ),
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ 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,
+ ),
+ ),
+ 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,
+ ),
+ ),
+ ),
+ ),
+ );
+ });
+ }
+}
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..723ede38d
--- /dev/null
+++ b/app/lib/pages/settings/creator_profile/creator_profile_provider.dart
@@ -0,0 +1,144 @@
+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();
+ final TextEditingController creatorEmailController = TextEditingController();
+ final TextEditingController paypalEmailController = TextEditingController();
+ final TextEditingController paypalMeLinkController = TextEditingController();
+ final GlobalKey formKey = GlobalKey();
+ bool showSubmitButton = false;
+ bool isLoading = false;
+ bool profileExists = false;
+
+ int totalUsage = 0;
+ double totalEarnings = 0.0;
+ 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();
+ }
+
+ void submitButtonStatus() {
+ if (creatorNameController.text.isNotEmpty &&
+ creatorEmailController.text.isNotEmpty &&
+ paypalEmailController.text.isNotEmpty &&
+ paypalMeLinkController.text.isNotEmpty) {
+ showSubmitButton = true;
+ } else {
+ showSubmitButton = false;
+ }
+ 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 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('Please complete your profile to receive payments.');
+ } else {
+ profileExists = true;
+ creatorNameController.text = res.creatorName;
+ creatorEmailController.text = res.creatorEmail;
+ 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 {
+ if (formKey.currentState!.validate()) {
+ setIsLoading(true);
+ var res = await updateCreatorProfileServer(
+ creatorNameController.text,
+ creatorEmailController.text,
+ paypalEmailController.text,
+ paypalMeLinkController.text,
+ );
+ if (res) {
+ AppSnackbar.showSnackbarSuccess('Creator profile details updated successfully');
+ } else {
+ AppSnackbar.showSnackbarError('Failed to update your creator profile details');
+ }
+ showSubmitButton = false;
+ profileExists = true;
+ setIsLoading(false);
+ } else {
+ AppSnackbar.showSnackbarError('Please fill all the fields correctly');
+ }
+ }
+
+ Future saveDetails() async {
+ if (formKey.currentState!.validate()) {
+ setIsLoading(true);
+ var profile = CreatorProfile(
+ creatorName: creatorNameController.text,
+ creatorEmail: creatorEmailController.text,
+ paypalDetails: PayPalDetails(
+ email: paypalEmailController.text,
+ paypalMeLink: paypalMeLinkController.text,
+ ),
+ isVerified: false,
+ );
+ var res = await saveCreatorProfile(profile);
+ if (res) {
+ profileExists = true;
+ AppSnackbar.showSnackbarSuccess('Creator profile details saved successfully');
+ } else {
+ AppSnackbar.showSnackbarError('Failed to update your creator profile details');
+ }
+ showSubmitButton = false;
+ setIsLoading(false);
+ } else {
+ AppSnackbar.showSnackbarError('Please fill all the fields correctly');
+ }
+ }
+}
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))),
+ );
+ }
+ }
+}
diff --git a/app/lib/pages/settings/page.dart b/app/lib/pages/settings/page.dart
index 13de76ce4..03c3cc7fd 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_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';
@@ -80,6 +81,12 @@ class _SettingsPageState extends State {
() => routeToPage(context, const ProfilePage()),
icon: Icons.person,
),
+ const SizedBox(height: 8),
+ getItemAddOn2(
+ 'Creator Dashboard',
+ () => routeToPage(context, const CreatorDashboard()),
+ icon: Icons.person,
+ ),
const SizedBox(height: 20),
getItemAddOn2(
'Device Settings',
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,
+ );
+ }
+}
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);
+}
diff --git a/backend/database/apps.py b/backend/database/apps.py
index 1623e8e2d..fd73ec9d2 100644
--- a/backend/database/apps.py
+++ b/backend/database/apps.py
@@ -30,6 +30,22 @@ 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 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 d437db132..8355d2679 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: eval(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
@@ -169,6 +197,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/database/users.py b/backend/database/users.py
index 7ebfbdc33..884f29f63 100644
--- a/backend/database/users.py
+++ b/backend/database/users.py
@@ -85,6 +85,32 @@ 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.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/app.py b/backend/models/app.py
index c8d82b1ff..1b9e3783b 100644
--- a/backend/models/app.py
+++ b/backend/models/app.py
@@ -128,3 +128,25 @@ 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
+ 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/models/users.py b/backend/models/users.py
index 5ea200c1a..84c3de503 100644
--- a/backend/models/users.py
+++ b/backend/models/users.py
@@ -1,5 +1,9 @@
+from datetime import datetime
from enum import Enum
+from pydantic import BaseModel
+from typing import Optional
+
class WebhookType(str, Enum):
audio_bytes = 'audio_bytes'
@@ -7,3 +11,35 @@ class WebhookType(str, Enum):
realtime_transcript = 'realtime_transcript'
memory_created = 'memory_created',
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_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/apps.py b/backend/routers/apps.py
index 9bb732691..f22db1f48 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
@@ -20,8 +21,9 @@
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
+from utils.user import get_user_creator_profile
router = APIRouter()
@@ -87,17 +89,60 @@ def create_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())
+ data['uid'] = uid
+ creator_profile = get_user_creator_profile(uid)
+ if not creator_profile:
+ 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'):
+ 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/apps', exist_ok=True)
+ file_path = f"_temp/apps/{file.filename}"
+ with open(file_path, 'wb') as f:
+ f.write(file.file.read())
+ 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)
+
+ # payment link
+ app = App(**data)
+ upsert_app_payment_link(app.id, app.is_paid, app.price, app.payment_plan)
+
+ 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)):
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:
@@ -110,9 +155,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'}
diff --git a/backend/routers/users.py b/backend/routers/users.py
index 6ed96b1ac..d6cd01e1b 100644
--- a/backend/routers/users.py
+++ b/backend/routers/users.py
@@ -1,20 +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, 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
+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()
@@ -38,6 +44,54 @@ 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: CreatorProfileRequest, uid: str = Depends(auth.get_current_user_uid)):
+ data = data.dict()
+ data['created_at'] = datetime.now(timezone.utc)
+ data['is_verified'] = False
+ 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(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(uid)
+ current_data.update(data)
+ current_data['updated_at'] = datetime.now(timezone.utc)
+ update_user_creator_profile(uid, current_data)
+ update_creator_details_for_user_apps(uid, current_data)
+ 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,
+ }
+
+
+@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 **************
# ***********************************************
@@ -226,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/apps.py b/backend/utils/apps.py
index c59038ee1..91bb7f3f8 100644
--- a/backend/utils/apps.py
+++ b/backend/utils/apps.py
@@ -13,7 +13,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_user_paid_app, get_user_paid_app
+ set_app_usage_count_cache, get_multiple_apps_usage_count_cache, get_multiple_apps_money_made_amount_cache, \
+ get_multiple_apps_enabled_count_cache, set_user_paid_app, get_user_paid_app
from models.app import App, UsageHistoryItem, UsageHistoryType
from utils import stripe
@@ -59,6 +60,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 = []
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)