Skip to content

Commit

Permalink
Improve Apps (#1260)
Browse files Browse the repository at this point in the history
- [x] Change visibility on the go
- [x] Delete App
- [x] Edit/Update App
- [x] Improve endpoints logic
- [x] Notification on App status change
- [x] Change Add app UI (also include new triggers)
- [x] Improve app detail UI
- [x] Restructure stuff
- [x] Setup instruction text or link
- [x] App review status indicator
- [x] Create App Model to simplify things
- [ ] Simplify external integration steps (probably later)

I guess I am missing few more things which I did already :/






https://github.com/user-attachments/assets/401d93f0-e6a2-4c33-9e3c-4487d7d61a80
  • Loading branch information
beastoin authored Nov 11, 2024
2 parents c517795 + 1125937 commit aaee3ba
Show file tree
Hide file tree
Showing 30 changed files with 2,902 additions and 1,151 deletions.
132 changes: 126 additions & 6 deletions app/lib/backend/http/api/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:path/path.dart';

Future<List<App>> retrieveApps() async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v3/plugins',
url: '${Env.apiBaseUrl}v1/apps',
headers: {},
body: '',
method: 'GET',
Expand All @@ -37,7 +37,7 @@ Future<List<App>> retrieveApps() async {

Future<bool> enableAppServer(String appId) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/plugins/enable?plugin_id=$appId',
url: '${Env.apiBaseUrl}v1/apps/enable?app_id=$appId',
headers: {},
method: 'POST',
body: '',
Expand All @@ -49,7 +49,7 @@ Future<bool> enableAppServer(String appId) async {

Future<bool> disableAppServer(String appId) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/plugins/disable?plugin_id=$appId',
url: '${Env.apiBaseUrl}v1/apps/disable?app_id=$appId',
headers: {},
method: 'POST',
body: '',
Expand All @@ -61,7 +61,7 @@ Future<bool> disableAppServer(String appId) async {

Future<void> reviewApp(String appId, double score, {String review = ''}) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/plugins/review?plugin_id=$appId',
url: '${Env.apiBaseUrl}v1/apps/review?app_id=$appId',
headers: {'Content-Type': 'application/json'},
method: 'POST',
body: jsonEncode({'score': score, review: review}),
Expand Down Expand Up @@ -141,11 +141,11 @@ Future<double> getAppMoneyMade(String pluginId) async {
Future<bool> submitAppServer(File file, Map<String, dynamic> appData) async {
var request = http.MultipartRequest(
'POST',
Uri.parse('${Env.apiBaseUrl}v3/plugins'),
Uri.parse('${Env.apiBaseUrl}v1/apps'),
);
request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
request.headers.addAll({'Authorization': await getAuthHeader()});
request.fields.addAll({'plugin_data': jsonEncode(appData)});
request.fields.addAll({'app_data': jsonEncode(appData)});
print(jsonEncode(appData));
try {
var streamedResponse = await request.send();
Expand All @@ -164,6 +164,34 @@ Future<bool> submitAppServer(File file, Map<String, dynamic> appData) async {
}
}

Future<bool> updateAppServer(File? file, Map<String, dynamic> appData) async {
var request = http.MultipartRequest(
'PATCH',
Uri.parse('${Env.apiBaseUrl}v1/apps/${appData['id']}'),
);
if (file != null) {
request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
}
request.headers.addAll({'Authorization': await getAuthHeader()});
request.fields.addAll({'app_data': jsonEncode(appData)});
print(jsonEncode(appData));
try {
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);

if (response.statusCode == 200) {
debugPrint('updateAppServer Response body: ${jsonDecode(response.body)}');
return true;
} else {
debugPrint('Failed to update app. Status code: ${response.statusCode}');
return false;
}
} catch (e) {
debugPrint('An error occurred updateAppServer: $e');
return false;
}
}

Future<List<Category>> getAppCategories() async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/plugin-categories',
Expand All @@ -183,6 +211,25 @@ Future<List<Category>> getAppCategories() async {
}
}

Future<List<AppCapability>> getAppCapabilitiesServer() async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/app-capabilities',
headers: {},
body: '',
method: 'GET',
);
try {
if (response == null || response.statusCode != 200) return [];
log('getAppCapabilities: ${response.body}');
var res = jsonDecode(response.body);
return AppCapability.fromJsonList(res);
} catch (e, stackTrace) {
debugPrint(e.toString());
CrashReporting.reportHandledCrash(e, stackTrace);
return [];
}
}

Future<List<TriggerEvent>> getTriggerEventsServer() async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/plugin-triggers',
Expand All @@ -201,3 +248,76 @@ Future<List<TriggerEvent>> getTriggerEventsServer() async {
return [];
}
}

Future<List<NotificationScope>> getNotificationScopesServer() async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/apps/proactive-notification-scopes',
headers: {},
body: '',
method: 'GET',
);
try {
if (response == null || response.statusCode != 200) return [];
log('getNotificationScopes: ${response.body}');
var res = jsonDecode(response.body);
return NotificationScope.fromJsonList(res);
} catch (e, stackTrace) {
debugPrint(e.toString());
CrashReporting.reportHandledCrash(e, stackTrace);
return [];
}
}

Future changeAppVisibilityServer(String appId, bool makePublic) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/apps/$appId/change-visibility?private=${!makePublic}',
headers: {},
body: '',
method: 'PATCH',
);
try {
if (response == null || response.statusCode != 200) return false;
log('changeAppVisibilityServer: ${response.body}');
return true;
} catch (e, stackTrace) {
debugPrint(e.toString());
CrashReporting.reportHandledCrash(e, stackTrace);
return false;
}
}

Future deleteAppServer(String appId) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/apps/$appId',
headers: {},
body: '',
method: 'DELETE',
);
try {
if (response == null || response.statusCode != 200) return false;
log('deleteAppServer: ${response.body}');
return true;
} catch (e, stackTrace) {
debugPrint(e.toString());
CrashReporting.reportHandledCrash(e, stackTrace);
return false;
}
}

Future<Map<String, dynamic>?> getAppDetailsServer(String appId) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/apps/$appId',
headers: {},
body: '',
method: 'GET',
);
try {
if (response == null || response.statusCode != 200) return null;
log('getAppDetailsServer: ${response.body}');
return jsonDecode(response.body);
} catch (e, stackTrace) {
debugPrint(e.toString());
CrashReporting.reportHandledCrash(e, stackTrace);
return null;
}
}
113 changes: 112 additions & 1 deletion app/lib/backend/schema/app.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:friend_private/widgets/extensions/string.dart';

class AppReview {
String uid;
DateTime ratedAt;
Expand Down Expand Up @@ -60,13 +62,15 @@ class ExternalIntegration {
String webhookUrl;
String? setupCompletedUrl;
String setupInstructionsFilePath;
bool isInstructionsUrl;
List<AuthStep> authSteps;

ExternalIntegration({
required this.triggersOn,
required this.webhookUrl,
required this.setupCompletedUrl,
required this.setupInstructionsFilePath,
required this.isInstructionsUrl,
this.authSteps = const [],
});

Expand All @@ -75,6 +79,7 @@ class ExternalIntegration {
triggersOn: json['triggers_on'],
webhookUrl: json['webhook_url'],
setupCompletedUrl: json['setup_completed_url'],
isInstructionsUrl: json['is_instructions_url'] ?? false,
setupInstructionsFilePath: json['setup_instructions_file_path'],
authSteps: json['auth_steps'] == null
? []
Expand All @@ -98,6 +103,7 @@ class ExternalIntegration {
'triggers_on': triggersOn,
'webhook_url': webhookUrl,
'setup_completed_url': setupCompletedUrl,
'is_instructions_url': isInstructionsUrl,
'setup_instructions_file_path': setupInstructionsFilePath,
'auth_steps': authSteps.map((e) => e.toJson()).toList(),
};
Expand Down Expand Up @@ -134,13 +140,17 @@ class AppUsageHistory {

class App {
String id;
String? uid;
String name;
String author;
String? email;
String category;
String status;
String description;
String image;
Set<String> capabilities;
bool private;

bool approved;
String? memoryPrompt;
String? chatPrompt;
ExternalIntegration? externalIntegration;
Expand All @@ -163,6 +173,11 @@ class App {
required this.description,
required this.image,
required this.capabilities,
required this.status,
this.uid,
this.email,
required this.category,
required this.approved,
this.memoryPrompt,
this.chatPrompt,
this.externalIntegration,
Expand All @@ -188,7 +203,12 @@ class App {

factory App.fromJson(Map<String, dynamic> json) {
return App(
category: json['category'],
approved: json['approved'],
status: json['status'],
id: json['id'],
email: json['email'],
uid: json['uid'],
name: json['name'],
author: json['author'],
description: json['description'],
Expand Down Expand Up @@ -217,6 +237,26 @@ class App {
}
}

bool isOwner(String uid) {
return this.uid == uid;
}

bool isUnderReview() {
return status == 'under-review';
}

bool isRejected() {
return status == 'rejected';
}

String getCategoryName() {
return category.decodeString.split('-').map((e) => e.capitalize()).join(' ');
}

List<AppCapability> getCapabilitiesFromIds(List<AppCapability> allCapabilities) {
return allCapabilities.where((e) => capabilities.contains(e.id)).toList();
}

Map<String, dynamic> toJson() {
return {
'id': id,
Expand All @@ -235,6 +275,12 @@ class App {
'deleted': deleted,
'enabled': enabled,
'installs': installs,
'private': private,
'category': category,
'approved': approved,
'status': status,
'uid': uid,
'email': email,
};
}

Expand Down Expand Up @@ -268,6 +314,44 @@ class Category {
}
}

class AppCapability {
String title;
String id;
List<TriggerEvent> triggerEvents = [];
List<NotificationScope> notificationScopes = [];
AppCapability({
required this.title,
required this.id,
this.triggerEvents = const [],
this.notificationScopes = const [],
});

factory AppCapability.fromJson(Map<String, dynamic> json) {
return AppCapability(
title: json['title'],
id: json['id'],
triggerEvents: TriggerEvent.fromJsonList(json['triggers'] ?? []),
notificationScopes: NotificationScope.fromJsonList(json['scopes'] ?? []),
);
}

toJson() {
return {
'title': title,
'id': id,
'triggers': triggerEvents.map((e) => e.toJson()).toList(),
'scopes': notificationScopes.map((e) => e.toJson()).toList(),
};
}

static List<AppCapability> fromJsonList(List<dynamic> jsonList) {
return jsonList.map((e) => AppCapability.fromJson(e)).toList();
}

bool hasTriggers() => triggerEvents.isNotEmpty;
bool hasScopes() => notificationScopes.isNotEmpty;
}

class TriggerEvent {
String title;
String id;
Expand All @@ -294,3 +378,30 @@ class TriggerEvent {
return jsonList.map((e) => TriggerEvent.fromJson(e)).toList();
}
}

class NotificationScope {
String title;
String id;
NotificationScope({
required this.title,
required this.id,
});

factory NotificationScope.fromJson(Map<String, dynamic> json) {
return NotificationScope(
title: json['title'],
id: json['id'],
);
}

toJson() {
return {
'title': title,
'id': id,
};
}

static List<NotificationScope> fromJsonList(List<dynamic> jsonList) {
return jsonList.map((e) => NotificationScope.fromJson(e)).toList();
}
}
Loading

0 comments on commit aaee3ba

Please sign in to comment.