♻️ Refactored and joined the Solar Network

This commit is contained in:
2024-03-17 20:22:46 +08:00
parent f3da8f5349
commit 5da657a73c
39 changed files with 386 additions and 345 deletions

View File

@ -3,34 +3,33 @@ 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:solaragent/firebase.dart';
import 'package:solaragent/preferences.dart';
import 'package:solaragent/screens/auth.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
final authClient = AuthGuard();
class AuthGuard {
static final AuthGuard _singleton = AuthGuard._internal();
AuthGuard();
final deviceEndpoint =
Uri.parse('https://id.smartsheep.studio/api/notifications/subscribe');
Uri.parse('https://id.solsynth.dev/api/notifications/subscribe');
final authorizationEndpoint =
Uri.parse('https://id.smartsheep.studio/auth/o/connect');
Uri.parse('https://id.solsynth.dev/auth/o/connect');
final tokenEndpoint =
Uri.parse('https://id.smartsheep.studio/api/auth/token');
Uri.parse('https://id.solsynth.dev/api/auth/token');
final userinfoEndpoint =
Uri.parse('https://id.smartsheep.studio/api/users/me');
final redirectUrl = Uri.parse('goatagent://auth');
Uri.parse('https://id.solsynth.dev/api/users/me');
final redirectUrl = Uri.parse('solaragent://auth');
static const clientId = "goatagent";
static const clientId = "solaragent";
static const clientSecret = "_F4%q2Eea3";
static const storage = FlutterSecureStorage();
static const storageKey = "identity";
static const profileKey = "profiles";
factory AuthGuard() {
return _singleton;
}
oauth2.Client? client;
Future<bool> pickClient() async {
@ -68,11 +67,6 @@ class AuthGuard {
var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
if (Platform.isAndroid || Platform.isIOS) {
// Let Goatpass know it is embed in an app
authorizationUrl = authorizationUrl.replace(
queryParameters: {"embedded": "yes"}
..addAll(authorizationUrl.queryParameters));
// Use WebView to get authorization url
var responseUrl = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(

View File

@ -67,7 +67,7 @@ class DefaultFirebaseOptions {
messagingSenderId: '659822066072',
projectId: 'smartsheep-hydrogen',
storageBucket: 'smartsheep-hydrogen.appspot.com',
iosBundleId: 'studio.smartsheep.goatagent',
iosBundleId: 'dev.solsynth.solaragent',
);
static const FirebaseOptions macos = FirebaseOptions(
@ -76,6 +76,6 @@ class DefaultFirebaseOptions {
messagingSenderId: '659822066072',
projectId: 'smartsheep-hydrogen',
storageBucket: 'smartsheep-hydrogen.appspot.com',
iosBundleId: 'studio.smartsheep.goatagent.RunnerTests',
iosBundleId: 'dev.solsynth.solaragent.RunnerTests',
);
}

View File

@ -1,114 +0,0 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:goatagent/screens/account.dart';
import 'package:goatagent/screens/dashboard.dart';
import 'package:goatagent/screens/notifications.dart';
class AgentNavigation extends StatefulWidget {
AgentNavigation({super.key});
static const items = [
{'label': 'Dashboard', 'icon': Icon(Icons.home)},
{'label': 'Notifications', 'icon': Icon(Icons.notifications)},
{'label': 'Account', 'icon': Icon(Icons.account_circle)},
];
final bottomDestinations = items
.map((element) => BottomNavigationBarItem(
icon: element["icon"] as Widget,
label: element["label"] as String,
))
.toList();
final railDestinations = items
.map((element) => NavigationRailDestination(
icon: element["icon"] as Widget,
label: Text(element["label"] as String),
))
.toList();
@override
State<AgentNavigation> createState() => _AgentNavigationState();
}
class _AgentNavigationState extends State<AgentNavigation> {
int _view = 0;
Future<void> initMessage(BuildContext context) async {
void navigate() {
setState(() {
_view = 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) {
final isLargeScreen = MediaQuery.of(context).size.width > 640;
final content = Stack(
children: [
Offstage(
offstage: _view != 0,
child: const DashboardScreen(),
),
Offstage(
offstage: _view != 1,
child: const NotificationScreen(),
),
Offstage(
offstage: _view != 2,
child: const AccountScreen(),
)
],
);
if (isLargeScreen) {
return Row(children: <Widget>[
NavigationRail(
selectedIndex: _view,
groupAlignment: 0,
onDestinationSelected: (int index) {
setState(() {
_view = index;
});
},
labelType: NavigationRailLabelType.all,
destinations: widget.railDestinations,
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(child: content),
]);
} else {
return Scaffold(
body: content,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _view,
showUnselectedLabels: false,
items: widget.bottomDestinations,
onTap: (index) {
setState(() {
_view = index;
});
},
),
);
}
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:goatagent/auth.dart';
import 'package:goatagent/firebase.dart';
import 'layouts/navigation.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/firebase.dart';
import 'package:solaragent/router.dart';
import 'package:solaragent/widgets/navigation.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -13,39 +13,51 @@ void main() async {
print(e);
}
await AuthGuard().pickClient();
await authClient.pickClient();
runApp(const GoatAgent());
runApp(const SolarAgent());
}
class GoatAgent extends StatelessWidget {
const GoatAgent({super.key});
class SolarAgent extends StatelessWidget {
const SolarAgent({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GoatAgent',
return MaterialApp.router(
title: 'SolarAgent',
theme: ThemeData(
brightness: Brightness.light,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.indigo,
accentColor: Colors.indigoAccent,
backgroundColor: Colors.white,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
accentColor: Colors.indigoAccent,
backgroundColor: Colors.white,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.indigo,
accentColor: Colors.indigoAccent,
backgroundColor: Colors.black,
brightness: Brightness.dark,
primarySwatch: Colors.indigo,
accentColor: Colors.indigoAccent,
backgroundColor: Colors.black,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: AgentNavigation(),
routerConfig: router,
builder: (context, child) => Overlay(
initialEntries: [
OverlayEntry(
builder: (context) => SafeArea(
child: Scaffold(
body: child,
bottomNavigationBar: const AgentNavigation(),
),
),
)
],
),
);
}
}

5
lib/preferences.dart Normal file
View File

@ -0,0 +1,5 @@
import 'package:shared_preferences/shared_preferences.dart';
getPreferences() async {
return await SharedPreferences.getInstance();
}

21
lib/router.dart Normal file
View File

@ -0,0 +1,21 @@
import 'package:go_router/go_router.dart';
import 'package:solaragent/screens/account.dart';
import 'package:solaragent/screens/dashboard.dart';
import 'package:solaragent/screens/notifications.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const DashboardScreen(),
),
GoRoute(
path: '/notifications',
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: '/account',
builder: (context, state) => const AccountScreen(),
)
],
);

View File

@ -16,9 +16,9 @@ class AboutScreen extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('GoatAgent',
Text('SolarAgent',
style: Theme.of(context).textTheme.headlineMedium),
Text('Goatworks Official Mobile Helper',
Text('Solarworks Official Mobile Helper',
style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 20),
FutureBuilder(
@ -37,7 +37,7 @@ class AboutScreen extends StatelessWidget {
const SizedBox(height: 10),
MaterialButton(
onPressed: () async {
await launchUrl(Uri.parse('https://smartsheep.studio'));
await launchUrl(Uri.parse('https://solsynth.dev'));
},
child: const Text('Official Website'),
),

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:goatagent/auth.dart';
import 'package:goatagent/screens/about.dart';
import 'package:goatagent/widgets/name_card.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/screens/about.dart';
import 'package:solaragent/widgets/name_card.dart';
class AccountScreen extends StatefulWidget {
const AccountScreen({super.key});
@ -17,7 +17,7 @@ class _AccountScreenState extends State<AccountScreen> {
@override
void initState() {
super.initState();
AuthGuard().isAuthorized().then((value) {
authClient.isAuthorized().then((value) {
setState(() {
isAuthorized = value;
});
@ -35,8 +35,8 @@ class _AccountScreenState extends State<AccountScreen> {
padding: const EdgeInsets.only(top: 20),
child: NameCard(
onLogin: () async {
await AuthGuard().login(context);
var authorized = await AuthGuard().isAuthorized();
await authClient.login(context);
var authorized = await authClient.isAuthorized();
setState(() {
isAuthorized = authorized;
});
@ -49,44 +49,43 @@ class _AccountScreenState extends State<AccountScreen> {
spacing: 5,
children: [
FutureBuilder(
future: AuthGuard().isAuthorized(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return Card(
elevation: 0,
child: InkWell(
splashColor: Colors.indigo.withAlpha(30),
onTap: () async {
AuthGuard().logout();
var authorized = await AuthGuard().isAuthorized();
setState(() {
isAuthorized = authorized;
});
},
child: const ListTile(
leading: Icon(Icons.logout),
title: Text('Logout'),
),
),
);
} else {
return Container();
}
future: authClient.isAuthorized(),
builder:
(BuildContext context, AsyncSnapshot<bool> snapshot) {
return (snapshot.hasData && snapshot.data == true)
? InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(40)),
splashColor: Colors.indigo.withAlpha(30),
onTap: () async {
authClient.logout();
var authorized =
await authClient.isAuthorized();
setState(() {
isAuthorized = authorized;
});
},
child: const ListTile(
leading: Icon(Icons.logout),
title: Text('Logout'),
),
)
: Container();
},
),
Card(
elevation: 0,
child: InkWell(
splashColor: Colors.indigo.withAlpha(30),
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => const AboutScreen(),
));
},
child: const ListTile(
leading: Icon(Icons.info_outline),
title: Text('About'),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(40)),
splashColor: Colors.indigo.withAlpha(30),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AboutScreen(),
));
},
child: const ListTile(
leading: Icon(Icons.info_outline),
title: Text('About'),
),
),
],

View File

@ -11,7 +11,7 @@ class AuthorizationScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Connect with Goatpass'),
title: const Text('Sign in'),
),
body: Stack(children: [
WebViewWidget(
@ -20,10 +20,10 @@ class AuthorizationScreen extends StatelessWidget {
..setBackgroundColor(Colors.white)
..setNavigationDelegate(NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('goatagent://auth')) {
if (request.url.startsWith('solaragent://auth')) {
Navigator.of(context).pop(request.url);
return NavigationDecision.prevent;
} else if (request.url.startsWith("https://id.smartsheep.studio/auth/register")) {
} else if (request.url.startsWith("https://solsynth.dev/auth/sign-up")) {
launchUrl(Uri.parse(request.url));
return NavigationDecision.prevent;
}

View File

@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:goatagent/screens/application.dart';
import 'package:solaragent/screens/application.dart';
import 'package:http/http.dart' as http;
class DashboardScreen extends StatefulWidget {
@ -14,7 +14,7 @@ class DashboardScreen extends StatefulWidget {
class _DashboardScreenState extends State<DashboardScreen> {
final client = http.Client();
final directoryEndpoint =
Uri.parse('https://id.smartsheep.studio/.well-known');
Uri.parse('https://id.solsynth.dev/.well-known');
List<dynamic> directory = List.empty();

View File

@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:goatagent/auth.dart';
import 'package:solaragent/auth.dart';
class NotificationScreen extends StatefulWidget {
const NotificationScreen({super.key});
@ -12,7 +12,7 @@ class NotificationScreen extends StatefulWidget {
class _NotificationScreenState extends State<NotificationScreen> {
final notificationEndpoint = Uri.parse(
'https://id.smartsheep.studio/api/notifications?skip=0&take=20');
'https://id.solsynth.dev/api/notifications?skip=0&take=25');
List<dynamic> notifications = List.empty();
@ -23,9 +23,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
}
Future<void> _pullNotifications() async {
if (await AuthGuard().isAuthorized()) {
await AuthGuard().pullProfiles();
var profiles = await AuthGuard().readProfiles();
if (await authClient.isAuthorized()) {
await authClient.pullProfiles();
var profiles = await authClient.readProfiles();
setState(() {
notifications = profiles['notifications'];
});
@ -33,11 +33,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
}
Future<void> _markAsRead(element) async {
if (AuthGuard().client != null) {
if (authClient.client != null) {
var id = element['id'];
var uri =
Uri.parse('https://id.smartsheep.studio/api/notifications/$id/read');
await AuthGuard().client!.put(uri);
Uri.parse('https://id.solsynth.dev/api/notifications/$id/read');
await authClient.client!.put(uri);
}
}

View File

@ -12,8 +12,8 @@ class NameCard extends StatelessWidget {
final void Function()? onCheck;
Future<CircleAvatar> _getAvatar() async {
if (await AuthGuard().isAuthorized()) {
final profiles = await AuthGuard().readProfiles();
if (await authClient.isAuthorized()) {
final profiles = await authClient.readProfiles();
return CircleAvatar(backgroundImage: NetworkImage(profiles["picture"]));
} else {
return const CircleAvatar(child: Icon(Icons.account_circle));
@ -21,8 +21,8 @@ class NameCard extends StatelessWidget {
}
Future<Column> _getDescribe() async {
if (await AuthGuard().isAuthorized()) {
final profiles = await AuthGuard().readProfiles();
if (await authClient.isAuthorized()) {
final profiles = await authClient.readProfiles();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -59,7 +59,7 @@ class NameCard extends StatelessWidget {
child: InkWell(
splashColor: Colors.indigo.withAlpha(30),
onTap: () async {
if (await AuthGuard().isAuthorized() && onCheck != null) {
if (await authClient.isAuthorized() && onCheck != null) {
onCheck!();
} else if (onLogin != null) {
onLogin!();

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:solaragent/router.dart';
class AgentNavigation extends StatefulWidget {
const AgentNavigation({super.key});
static const List<(String, NavigationDestination)> destinations = [
('/', NavigationDestination(icon: Icon(Icons.home), label: 'Home')),
('/notifications', NavigationDestination(icon: Icon(Icons.notifications), label: 'Notifications')),
('/account', NavigationDestination(icon: Icon(Icons.account_circle), label: 'Account')),
];
@override
State<AgentNavigation> createState() => _AgentNavigationState();
}
class _AgentNavigationState extends State<AgentNavigation> {
int currentPage = 0;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return NavigationBar(
selectedIndex: currentPage,
destinations: AgentNavigation.destinations
.map(((String, NavigationDestination) e) => e.$2)
.toList(),
onDestinationSelected: (index) {
router.go(AgentNavigation.destinations[index].$1);
setState(() => currentPage = index);
},
);
}
}