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 {
// 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 {

View File

@ -3,5 +3,5 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />
</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_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<bool> 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<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 {
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<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() {
try {
storage.delete(key: profileKey);

View File

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

View File

@ -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<AgentNavigation> createState() => _AgentNavigationState();
@ -26,6 +31,32 @@ class AgentNavigation extends StatefulWidget {
class _AgentNavigationState extends State<AgentNavigation> {
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
Widget build(BuildContext context) {
return NavigationBar(

View File

@ -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<NavigatorState>(),
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),
),
)
]);
},
);

View File

@ -30,14 +30,17 @@ class _AccountScreenState extends State<AccountScreen> {
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(),

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"]),
));
},
),
],
),
),
),
),
);
}
}