Notifications

This commit is contained in:
LittleSheep 2024-02-08 15:19:37 +08:00
parent 7ed4045a8c
commit b5f77c2736
9 changed files with 243 additions and 28 deletions

View File

@ -41,7 +41,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "studio.smartsheep.goatagent" applicationId "studio.smartsheep.goatagent"
// You can update the following values to match your application needs. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
@ -49,6 +48,10 @@ android {
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
manifestPlaceholders = [
applicationName : 'android.app.Application',
appAuthRedirectScheme: 'goatagent'
]
} }
buildTypes { buildTypes {

View File

@ -3,5 +3,5 @@
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@ -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);
}
}

View File

@ -3,12 +3,15 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:goatagent/firebase.dart';
import 'package:goatagent/screens/auth.dart'; import 'package:goatagent/screens/auth.dart';
import 'package:oauth2/oauth2.dart' as oauth2; import 'package:oauth2/oauth2.dart' as oauth2;
class AuthGuard { class AuthGuard {
static final AuthGuard _singleton = AuthGuard._internal(); static final AuthGuard _singleton = AuthGuard._internal();
final deviceEndpoint =
Uri.parse('https://id.smartsheep.studio/api/notifications/subscribe');
final authorizationEndpoint = final authorizationEndpoint =
Uri.parse('https://id.smartsheep.studio/auth/o/connect'); Uri.parse('https://id.smartsheep.studio/auth/o/connect');
final tokenEndpoint = final tokenEndpoint =
@ -32,12 +35,17 @@ class AuthGuard {
Future<bool> pickClient() async { Future<bool> pickClient() async {
if (await storage.containsKey(key: storageKey)) { if (await storage.containsKey(key: storageKey)) {
var credentials = try {
oauth2.Credentials.fromJson((await storage.read(key: storageKey))!); var credentials =
client = oauth2.Client(credentials, oauth2.Credentials.fromJson((await storage.read(key: storageKey))!);
identifier: clientId, secret: clientSecret); client = oauth2.Client(credentials,
print(await storage.readAll()); identifier: clientId, secret: clientSecret);
return true; await pullProfiles();
return true;
} catch (e) {
logout();
return false;
}
} else { } else {
return false; return false;
} }
@ -78,18 +86,46 @@ class AuthGuard {
} }
} }
Future<void> pullProfiles() async {
if (client != null) {
var userinfo = await client!.get(userinfoEndpoint);
storage.write(key: profileKey, value: utf8.decode(userinfo.bodyBytes));
}
}
Future<void> login(BuildContext context) async { Future<void> login(BuildContext context) async {
try { try {
client = await createClient(context); client = await createClient(context);
var userinfo = await client!.read(userinfoEndpoint);
storage.write(key: profileKey, value: userinfo);
storage.write(key: storageKey, value: client!.credentials.toJson()); storage.write(key: storageKey, value: client!.credentials.toJson());
await pullProfiles();
await subscribeNotify();
} catch (e) { } catch (e) {
print(e); print(e);
} }
} }
Future<void> 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() { void logout() {
try { try {
storage.delete(key: profileKey); storage.delete(key: profileKey);

View File

@ -11,6 +11,16 @@ void initializeFirebase() async {
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
await FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
FlutterError.onError = (errorDetails) { FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
}; };

View File

@ -1,3 +1,4 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -11,13 +12,17 @@ class AgentNavigation extends StatefulWidget {
icon: Icon(Icons.home), icon: Icon(Icons.home),
label: 'Dashboard', label: 'Dashboard',
), ),
NavigationDestination(
icon: Icon(Icons.notifications),
label: 'Notifications',
),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.account_circle), icon: Icon(Icons.account_circle),
label: 'Account', label: 'Account',
) )
]; ];
static const destinations = ["/", "/account"]; static const destinations = ["/", "/notifications", "/account"];
@override @override
State<AgentNavigation> createState() => _AgentNavigationState(); State<AgentNavigation> createState() => _AgentNavigationState();
@ -26,6 +31,32 @@ class AgentNavigation extends StatefulWidget {
class _AgentNavigationState extends State<AgentNavigation> { class _AgentNavigationState extends State<AgentNavigation> {
int _selected = 0; int _selected = 0;
Future<void> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return NavigationBar( return NavigationBar(

View File

@ -1,10 +1,11 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:goatagent/auth.dart'; import 'package:goatagent/auth.dart';
import 'package:goatagent/screens/auth.dart';
import 'package:goatagent/firebase.dart'; import 'package:goatagent/firebase.dart';
import 'package:goatagent/screens/account.dart'; import 'package:goatagent/screens/account.dart';
import 'package:goatagent/screens/dashboard.dart'; import 'package:goatagent/screens/dashboard.dart';
import 'package:goatagent/screens/notifications.dart';
import 'layouts/navigation.dart'; import 'layouts/navigation.dart';
@ -21,9 +22,18 @@ class GoatAgent extends StatelessWidget {
final _router = GoRouter( final _router = GoRouter(
navigatorKey: GlobalKey<NavigatorState>(), navigatorKey: GlobalKey<NavigatorState>(),
routes: [ routes: [
GoRoute(path: '/', builder: (context, state) => const DashboardScreen()),
GoRoute( 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) { builder: (BuildContext context, Widget? child) {
return Overlay(initialEntries: [ return Overlay(initialEntries: [
OverlayEntry( OverlayEntry(
builder: (context) => Scaffold( builder: (context) => Scaffold(
body: child, body: child,
// bottomNavigationBar: const AgentBottomNavigation() // bottomNavigationBar: const AgentBottomNavigation()
bottomNavigationBar: AgentNavigation(router: _router), bottomNavigationBar: AgentNavigation(router: _router),
)) ),
)
]); ]);
}, },
); );

View File

@ -30,14 +30,17 @@ class _AccountScreenState extends State<AccountScreen> {
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20),
child: Column( child: Column(
children: [ children: [
NameCard( Padding(
onLogin: () async { padding: const EdgeInsets.only(top: 20),
await AuthGuard().login(context); child: NameCard(
var authorized = await AuthGuard().isAuthorized(); onLogin: () async {
setState(() { await AuthGuard().login(context);
isAuthorized = authorized; var authorized = await AuthGuard().isAuthorized();
}); setState(() {
}, isAuthorized = authorized;
});
},
),
), ),
FutureBuilder( FutureBuilder(
future: AuthGuard().isAuthorized(), future: AuthGuard().isAuthorized(),

View File

@ -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<NotificationScreen> createState() => _NotificationScreenState();
}
class _NotificationScreenState extends State<NotificationScreen> {
final notificationEndpoint = Uri.parse(
'https://id.smartsheep.studio/api/notifications?skip=0&take=20');
List<dynamic> notifications = List.empty();
@override
void initState() {
super.initState();
_pullNotifications();
}
Future<void> _pullNotifications() async {
if (await AuthGuard().isAuthorized()) {
await AuthGuard().pullProfiles();
var profiles = await AuthGuard().readProfiles();
setState(() {
notifications = profiles['notifications'];
});
}
}
Future<void> _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"]),
));
},
),
],
),
),
),
),
);
}
}