diff --git a/android/app/build.gradle b/android/app/build.gradle
index 37ac1c4..089b185 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -41,7 +41,6 @@ android {
}
defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "studio.smartsheep.goatagent"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
@@ -49,6 +48,10 @@ android {
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
+ manifestPlaceholders = [
+ applicationName : 'android.app.Application',
+ appAuthRedirectScheme: 'goatagent'
+ ]
}
buildTypes {
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 399f698..4d95910 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -3,5 +3,5 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
-
+
diff --git a/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java b/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java
new file mode 100644
index 0000000..752fc18
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java
@@ -0,0 +1,25 @@
+// Generated file.
+//
+// If you wish to remove Flutter's multidex support, delete this entire file.
+//
+// Modifications to this file should be done in a copy under a different name
+// as this file may be regenerated.
+
+package io.flutter.app;
+
+import android.app.Application;
+import android.content.Context;
+import androidx.annotation.CallSuper;
+import androidx.multidex.MultiDex;
+
+/**
+ * Extension of {@link android.app.Application}, adding multidex support.
+ */
+public class FlutterMultiDexApplication extends Application {
+ @Override
+ @CallSuper
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ MultiDex.install(this);
+ }
+}
diff --git a/lib/auth.dart b/lib/auth.dart
index 1828bfb..381484a 100644
--- a/lib/auth.dart
+++ b/lib/auth.dart
@@ -3,12 +3,15 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:goatagent/firebase.dart';
import 'package:goatagent/screens/auth.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
class AuthGuard {
static final AuthGuard _singleton = AuthGuard._internal();
+ final deviceEndpoint =
+ Uri.parse('https://id.smartsheep.studio/api/notifications/subscribe');
final authorizationEndpoint =
Uri.parse('https://id.smartsheep.studio/auth/o/connect');
final tokenEndpoint =
@@ -32,12 +35,17 @@ class AuthGuard {
Future pickClient() async {
if (await storage.containsKey(key: storageKey)) {
- var credentials =
- oauth2.Credentials.fromJson((await storage.read(key: storageKey))!);
- client = oauth2.Client(credentials,
- identifier: clientId, secret: clientSecret);
- print(await storage.readAll());
- return true;
+ try {
+ var credentials =
+ oauth2.Credentials.fromJson((await storage.read(key: storageKey))!);
+ client = oauth2.Client(credentials,
+ identifier: clientId, secret: clientSecret);
+ await pullProfiles();
+ return true;
+ } catch (e) {
+ logout();
+ return false;
+ }
} else {
return false;
}
@@ -78,18 +86,46 @@ class AuthGuard {
}
}
+ Future pullProfiles() async {
+ if (client != null) {
+ var userinfo = await client!.get(userinfoEndpoint);
+ storage.write(key: profileKey, value: utf8.decode(userinfo.bodyBytes));
+ }
+ }
+
Future login(BuildContext context) async {
try {
client = await createClient(context);
- var userinfo = await client!.read(userinfoEndpoint);
-
- storage.write(key: profileKey, value: userinfo);
storage.write(key: storageKey, value: client!.credentials.toJson());
+
+ await pullProfiles();
+ await subscribeNotify();
} catch (e) {
print(e);
}
}
+ Future subscribeNotify() async {
+ if (client == null) {
+ return;
+ }
+
+ var token = await initializeFirebaseMessaging();
+ if (token == null) {
+ print("failed to initialize firebase messaging...");
+ return;
+ }
+
+ var response = await client!.post(
+ deviceEndpoint,
+ headers: {"Content-Type": "application/json"},
+ body: jsonEncode({"device_id": token, "provider": "firebase"}),
+ );
+ if (response.statusCode != 200) {
+ print(response.body);
+ }
+ }
+
void logout() {
try {
storage.delete(key: profileKey);
diff --git a/lib/firebase.dart b/lib/firebase.dart
index 7fb29be..86c51de 100644
--- a/lib/firebase.dart
+++ b/lib/firebase.dart
@@ -11,6 +11,16 @@ void initializeFirebase() async {
options: DefaultFirebaseOptions.currentPlatform,
);
+ await FirebaseMessaging.instance.requestPermission(
+ alert: true,
+ announcement: false,
+ badge: true,
+ carPlay: false,
+ criticalAlert: false,
+ provisional: false,
+ sound: true,
+ );
+
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
diff --git a/lib/layouts/navigation.dart b/lib/layouts/navigation.dart
index 63bc216..967b770 100644
--- a/lib/layouts/navigation.dart
+++ b/lib/layouts/navigation.dart
@@ -1,3 +1,4 @@
+import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -11,13 +12,17 @@ class AgentNavigation extends StatefulWidget {
icon: Icon(Icons.home),
label: 'Dashboard',
),
+ NavigationDestination(
+ icon: Icon(Icons.notifications),
+ label: 'Notifications',
+ ),
NavigationDestination(
icon: Icon(Icons.account_circle),
label: 'Account',
)
];
- static const destinations = ["/", "/account"];
+ static const destinations = ["/", "/notifications", "/account"];
@override
State createState() => _AgentNavigationState();
@@ -26,6 +31,32 @@ class AgentNavigation extends StatefulWidget {
class _AgentNavigationState extends State {
int _selected = 0;
+ Future initMessage(BuildContext context) async {
+ void navigate() {
+ widget.router.push("/notifications");
+ setState(() {
+ _selected = 1;
+ });
+ }
+
+ RemoteMessage? initialMessage =
+ await FirebaseMessaging.instance.getInitialMessage();
+
+ if (initialMessage != null) {
+ navigate();
+ }
+
+ FirebaseMessaging.onMessageOpenedApp.listen((event) {
+ navigate();
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ initMessage(context);
+ }
+
@override
Widget build(BuildContext context) {
return NavigationBar(
diff --git a/lib/main.dart b/lib/main.dart
index 30bb1f0..ecb1e94 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,10 +1,11 @@
+import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:goatagent/auth.dart';
-import 'package:goatagent/screens/auth.dart';
import 'package:goatagent/firebase.dart';
import 'package:goatagent/screens/account.dart';
import 'package:goatagent/screens/dashboard.dart';
+import 'package:goatagent/screens/notifications.dart';
import 'layouts/navigation.dart';
@@ -21,9 +22,18 @@ class GoatAgent extends StatelessWidget {
final _router = GoRouter(
navigatorKey: GlobalKey(),
routes: [
- GoRoute(path: '/', builder: (context, state) => const DashboardScreen()),
GoRoute(
- path: '/account', builder: (context, state) => const AccountScreen())
+ path: '/',
+ builder: (context, state) => const DashboardScreen(),
+ ),
+ GoRoute(
+ path: '/notifications',
+ builder: (context, state) => const NotificationScreen(),
+ ),
+ GoRoute(
+ path: '/account',
+ builder: (context, state) => const AccountScreen(),
+ ),
],
);
@@ -41,11 +51,12 @@ class GoatAgent extends StatelessWidget {
builder: (BuildContext context, Widget? child) {
return Overlay(initialEntries: [
OverlayEntry(
- builder: (context) => Scaffold(
- body: child,
- // bottomNavigationBar: const AgentBottomNavigation()
- bottomNavigationBar: AgentNavigation(router: _router),
- ))
+ builder: (context) => Scaffold(
+ body: child,
+ // bottomNavigationBar: const AgentBottomNavigation()
+ bottomNavigationBar: AgentNavigation(router: _router),
+ ),
+ )
]);
},
);
diff --git a/lib/screens/account.dart b/lib/screens/account.dart
index 2d60910..079b701 100644
--- a/lib/screens/account.dart
+++ b/lib/screens/account.dart
@@ -30,14 +30,17 @@ class _AccountScreenState extends State {
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20),
child: Column(
children: [
- NameCard(
- onLogin: () async {
- await AuthGuard().login(context);
- var authorized = await AuthGuard().isAuthorized();
- setState(() {
- isAuthorized = authorized;
- });
- },
+ Padding(
+ padding: const EdgeInsets.only(top: 20),
+ child: NameCard(
+ onLogin: () async {
+ await AuthGuard().login(context);
+ var authorized = await AuthGuard().isAuthorized();
+ setState(() {
+ isAuthorized = authorized;
+ });
+ },
+ ),
),
FutureBuilder(
future: AuthGuard().isAuthorized(),
diff --git a/lib/screens/notifications.dart b/lib/screens/notifications.dart
new file mode 100644
index 0000000..b6c63fe
--- /dev/null
+++ b/lib/screens/notifications.dart
@@ -0,0 +1,96 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:goatagent/auth.dart';
+
+class NotificationScreen extends StatefulWidget {
+ const NotificationScreen({super.key});
+
+ @override
+ State createState() => _NotificationScreenState();
+}
+
+class _NotificationScreenState extends State {
+ final notificationEndpoint = Uri.parse(
+ 'https://id.smartsheep.studio/api/notifications?skip=0&take=20');
+
+ List notifications = List.empty();
+
+ @override
+ void initState() {
+ super.initState();
+ _pullNotifications();
+ }
+
+ Future _pullNotifications() async {
+ if (await AuthGuard().isAuthorized()) {
+ await AuthGuard().pullProfiles();
+ var profiles = await AuthGuard().readProfiles();
+ setState(() {
+ notifications = profiles['notifications'];
+ });
+ }
+ }
+
+ Future _markAsRead(element) async {
+ if (AuthGuard().client != null) {
+ var id = element['id'];
+ var uri =
+ Uri.parse('https://id.smartsheep.studio/api/notifications/$id/read');
+ await AuthGuard().client!.put(uri);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.only(left: 20, right: 20, top: 30),
+ child: RefreshIndicator(
+ onRefresh: _pullNotifications,
+ child: CustomScrollView(
+ slivers: [
+ notifications.isEmpty
+ ? const SliverToBoxAdapter(
+ child: Card(
+ child: Padding(
+ padding: EdgeInsets.all(10),
+ child: ListTile(
+ leading: Icon(Icons.check),
+ title: Text('You\'re done!'),
+ subtitle: Text(
+ 'There are no notifications unread for you.'),
+ ),
+ ),
+ ),
+ )
+ : SliverList.builder(
+ itemCount: notifications.length,
+ itemBuilder: (BuildContext context, int index) {
+ var element = notifications[index];
+ return Dismissible(
+ key: Key('notification-$index'),
+ onDismissed: (direction) {
+ var subject = element["subject"];
+ _markAsRead(element).then((value) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content:
+ Text('「$subject」 mark as read')));
+ });
+ },
+ child: ListTile(
+ title: Text(element["subject"]),
+ subtitle: Text(element["content"]),
+ ));
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}