diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
index abc9cf8..db8477b 100644
--- a/flutter/android/app/src/main/AndroidManifest.xml
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,9 @@
+
+
+
);
RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
+ print('[REQUEST|FIN] [${method}] ${name}');
return result;
} else {
RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
+ print('[REQUEST|FIN] [${method}] ${name}');
return null as T;
}
} catch (exc, trace) {
@@ -120,7 +124,7 @@ class APIClient {
method: 'GET',
relURL: '/users/$uid',
fn: null,
- auth: KeyTokenAuth(userId: uid, token: tok),
+ auth: KeyTokenAuth(userId: uid, tokenAdmin: tok, tokenSend: ''),
);
return true;
} catch (e) {
@@ -138,7 +142,7 @@ class APIClient {
);
}
- static Future addClient(KeyTokenAuth? auth, String fcmToken, String agentModel, String agentVersion, String clientType) async {
+ static Future addClient(KeyTokenAuth? auth, String fcmToken, String agentModel, String agentVersion, String? descriptionName, String clientType) async {
return await _request(
name: 'addClient',
method: 'POST',
@@ -148,13 +152,14 @@ class APIClient {
'agent_model': agentModel,
'agent_version': agentVersion,
'client_type': clientType,
+ 'description_name': descriptionName,
},
fn: Client.fromJson,
auth: auth,
);
}
- static Future updateClient(KeyTokenAuth? auth, String clientID, String fcmToken, String agentModel, String agentVersion) async {
+ static Future updateClient(KeyTokenAuth? auth, String clientID, String fcmToken, String agentModel, String? descriptionName, String agentVersion) async {
return await _request(
name: 'updateClient',
method: 'PUT',
@@ -163,6 +168,7 @@ class APIClient {
'fcm_token': fcmToken,
'agent_model': agentModel,
'agent_version': agentVersion,
+ 'description_name': descriptionName,
},
fn: Client.fromJson,
auth: auth,
@@ -235,4 +241,22 @@ class APIClient {
auth: auth,
);
}
+
+ static Future createUserWithClient(String? username, String clientFcmToken, String clientAgentModel, String clientAgentVersion, String? clientDescriptionName, String clientType) async {
+ return await _request(
+ name: 'createUserWithClient',
+ method: 'POST',
+ relURL: 'users',
+ jsonBody: {
+ 'username': username,
+ 'fcm_token': clientFcmToken,
+ 'agent_model': clientAgentModel,
+ 'agent_version': clientAgentVersion,
+ 'description_name': clientDescriptionName,
+ 'client_type': clientType,
+ 'no_client': false,
+ },
+ fn: UserWithClientsAndKeys.fromJson,
+ );
+ }
}
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
index dab5914..32bbca4 100644
--- a/flutter/lib/main.dart
+++ b/flutter/lib/main.dart
@@ -14,6 +14,8 @@ import 'package:toastification/toastification.dart';
import 'firebase_options.dart';
void main() async {
+ print('[INIT] Application starting...');
+
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
@@ -43,7 +45,7 @@ void main() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
- final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true);
+ await FirebaseMessaging.instance.requestPermission(provisional: true);
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
try {
@@ -55,6 +57,15 @@ void main() async {
ApplicationLog.error('Failed to listen to token refresh events: ' + (err?.toString() ?? ''));
});
+ try {
+ final fcmToken = await FirebaseMessaging.instance.getToken();
+ if (fcmToken != null) {
+ setFirebaseToken(fcmToken);
+ }
+ } catch (exc, trace) {
+ ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
+ }
+
ApplicationLog.debug('Application started');
runApp(
@@ -69,14 +80,25 @@ void main() async {
}
void setFirebaseToken(String fcmToken) async {
- ApplicationLog.info('New firebase token: $fcmToken');
final acc = UserAccount();
+
+ final oldToken = Globals().getPrefFCMToken();
+
+ if (oldToken != null && oldToken == fcmToken && acc.client != null && acc.client!.fcmToken == fcmToken) {
+ ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken');
+ return;
+ }
+
+ ApplicationLog.info('New firebase token received', additional: 'Token: $fcmToken (old: $oldToken)');
+
+ await Globals().setPrefFCMToken(fcmToken);
+
if (acc.auth != null) {
if (acc.client == null) {
- final client = await APIClient.addClient(acc.auth, fcmToken, Globals().platform, Globals().version, Globals().clientType);
+ final client = await APIClient.addClient(acc.auth, fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
acc.setClient(client);
} else {
- final client = await APIClient.updateClient(acc.auth, acc.client!.clientID, fcmToken, Globals().platform, Globals().version);
+ final client = await APIClient.updateClient(acc.auth, acc.client!.clientID, fcmToken, Globals().deviceModel, Globals().hostname, Globals().version);
acc.setClient(client);
}
}
diff --git a/flutter/lib/models/channel.dart b/flutter/lib/models/channel.dart
index 337e19d..51b40c1 100644
--- a/flutter/lib/models/channel.dart
+++ b/flutter/lib/models/channel.dart
@@ -38,33 +38,18 @@ class Channel {
}
}
-class ChannelWithSubscription extends Channel {
+class ChannelWithSubscription {
+ final Channel channel;
final Subscription subscription;
ChannelWithSubscription({
- required super.channelID,
- required super.ownerUserID,
- required super.internalName,
- required super.displayName,
- required super.descriptionName,
- required super.subscribeKey,
- required super.timestampCreated,
- required super.timestampLastSent,
- required super.messagesSent,
+ required this.channel,
required this.subscription,
});
factory ChannelWithSubscription.fromJson(Map json) {
return ChannelWithSubscription(
- channelID: json['channel_id'] as String,
- ownerUserID: json['owner_user_id'] as String,
- internalName: json['internal_name'] as String,
- displayName: json['display_name'] as String,
- descriptionName: json['description_name'] as String?,
- subscribeKey: json['subscribe_key'] as String?,
- timestampCreated: json['timestamp_created'] as String,
- timestampLastSent: json['timestamp_lastsent'] as String?,
- messagesSent: json['messages_sent'] as int,
+ channel: Channel.fromJson(json),
subscription: Subscription.fromJson(json['subscription'] as Map),
);
}
diff --git a/flutter/lib/models/client.dart b/flutter/lib/models/client.dart
index 895d90c..c448ee2 100644
--- a/flutter/lib/models/client.dart
+++ b/flutter/lib/models/client.dart
@@ -28,7 +28,7 @@ class Client {
timestampCreated: json['timestamp_created'] as String,
agentModel: json['agent_model'] as String,
agentVersion: json['agent_version'] as String,
- descriptionName: json['description_name'] as String?,
+ descriptionName: json.containsKey('description_name') ? (json['description_name'] as String?) : null, //TODO change once API is updated / branch is merged
);
}
diff --git a/flutter/lib/models/key_token_auth.dart b/flutter/lib/models/key_token_auth.dart
index a0be225..4fb503b 100644
--- a/flutter/lib/models/key_token_auth.dart
+++ b/flutter/lib/models/key_token_auth.dart
@@ -1,6 +1,11 @@
class KeyTokenAuth {
final String userId;
- final String token;
+ final String tokenAdmin;
+ final String tokenSend;
- KeyTokenAuth({required this.userId, required this.token});
+ KeyTokenAuth({
+ required this.userId,
+ required this.tokenAdmin,
+ required this.tokenSend,
+ });
}
diff --git a/flutter/lib/models/user.dart b/flutter/lib/models/user.dart
index c8f4eb8..4a9b140 100644
--- a/flutter/lib/models/user.dart
+++ b/flutter/lib/models/user.dart
@@ -1,3 +1,5 @@
+import 'package:simplecloudnotifier/models/client.dart';
+
class User {
final String userID;
final String? username;
@@ -62,3 +64,29 @@ class User {
);
}
}
+
+class UserWithClientsAndKeys {
+ final User user;
+ final List clients;
+ final String sendKey;
+ final String readKey;
+ final String adminKey;
+
+ UserWithClientsAndKeys({
+ required this.user,
+ required this.clients,
+ required this.sendKey,
+ required this.readKey,
+ required this.adminKey,
+ });
+
+ factory UserWithClientsAndKeys.fromJson(Map json) {
+ return UserWithClientsAndKeys(
+ user: User.fromJson(json),
+ clients: Client.fromJsonArray(json['clients'] as List),
+ sendKey: json['send_key'] as String,
+ readKey: json['read_key'] as String,
+ adminKey: json['admin_key'] as String,
+ );
+ }
+}
diff --git a/flutter/lib/pages/account/account.dart b/flutter/lib/pages/account/account.dart
index 903933e..130c47a 100644
--- a/flutter/lib/pages/account/account.dart
+++ b/flutter/lib/pages/account/account.dart
@@ -1,10 +1,16 @@
+import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
+import 'package:simplecloudnotifier/models/key_token_auth.dart';
import 'package:simplecloudnotifier/models/user.dart';
+import 'package:simplecloudnotifier/pages/account/login.dart';
+import 'package:simplecloudnotifier/state/application_log.dart';
+import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/user_account.dart';
+import 'package:simplecloudnotifier/utils/toaster.dart';
class AccountRootPage extends StatefulWidget {
const AccountRootPage({super.key});
@@ -22,6 +28,8 @@ class _AccountRootPageState extends State {
late UserAccount userAcc;
+ bool loading = false;
+
@override
void initState() {
super.initState();
@@ -102,25 +110,54 @@ class _AccountRootPageState extends State {
}
Widget buildNoAuth(BuildContext context) {
- return Center(
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
- onPressed: () {
- //TODO
- },
- child: const Text('Use existing account'),
- ),
+ if (!loading)
+ Center(
+ child: Container(
+ width: 200,
+ height: 200,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.secondary,
+ borderRadius: BorderRadius.circular(100),
+ ),
+ child: Center(child: FaIcon(FontAwesomeIcons.userSecret, size: 96, color: Theme.of(context).colorScheme.onSecondary)),
+ ),
+ ),
+ if (loading)
+ Center(
+ child: Container(
+ width: 200,
+ height: 200,
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.secondary,
+ borderRadius: BorderRadius.circular(100),
+ ),
+ child: Center(child: CircularProgressIndicator(color: Theme.of(context).colorScheme.onSecondary)),
+ ),
+ ),
const SizedBox(height: 32),
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
onPressed: () {
- //TODO
+ if (loading) return;
+ createNewAccount();
},
child: const Text('Create new account'),
),
+ const SizedBox(height: 16),
+ FilledButton.tonal(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 24), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12)),
+ onPressed: () {
+ if (loading) return;
+ Navigator.push(context, MaterialPageRoute(builder: (context) => AccountLoginPage()));
+ },
+ child: const Text('Use existing account'),
+ ),
],
),
);
@@ -391,4 +428,40 @@ class _AccountRootPageState extends State {
),
);
}
+
+ void createNewAccount() async {
+ setState(() => loading = true);
+
+ final acc = Provider.of(context, listen: false);
+
+ try {
+ final notificationSettings = await FirebaseMessaging.instance.requestPermission(provisional: true);
+
+ if (notificationSettings.authorizationStatus == AuthorizationStatus.denied) {
+ Toaster.error("Missing Permission", 'Please allow notifications to create an account');
+ return;
+ }
+
+ final fcmToken = await FirebaseMessaging.instance.getToken();
+
+ if (fcmToken == null) {
+ Toaster.warn("Missing Token", 'No FCM Token found, please allow notifications, ensure you have a network connection and restart the app');
+ return;
+ }
+
+ await Globals().setPrefFCMToken(fcmToken);
+
+ final user = await APIClient.createUserWithClient(null, fcmToken, Globals().platform, Globals().version, Globals().hostname, Globals().clientType);
+
+ acc.setUser(user.user);
+ acc.setToken(KeyTokenAuth(userId: user.user.userID, tokenAdmin: user.adminKey, tokenSend: user.sendKey));
+ acc.setClient(user.clients[0]);
+ await acc.save();
+ } catch (exc, trace) {
+ ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace);
+ Toaster.error("Error", 'Failed to create user account');
+ } finally {
+ setState(() => loading = false);
+ }
+ }
}
diff --git a/flutter/lib/pages/account/login.dart b/flutter/lib/pages/account/login.dart
index 3ed8361..7ce7795 100644
--- a/flutter/lib/pages/account/login.dart
+++ b/flutter/lib/pages/account/login.dart
@@ -54,8 +54,8 @@ class _AccountLoginPageState extends State {
),
),
const SizedBox(height: 16),
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: _login,
child: const Text('Login'),
),
@@ -66,7 +66,7 @@ class _AccountLoginPageState extends State {
void _login() async {
final msgr = ScaffoldMessenger.of(context);
- final prov = Provider.of(context, listen: false);
+ final acc = Provider.of(context, listen: false);
try {
final uid = _ctrlUserID.text;
@@ -79,8 +79,8 @@ class _AccountLoginPageState extends State {
content: Text('Data ok'), //TODO use toast?
),
);
- prov.setToken(KeyTokenAuth(userId: uid, token: tok));
- await prov.save();
+ acc.setToken(KeyTokenAuth(userId: uid, tokenAdmin: tok, tokenSend: '')); //TOTO send token
+ await acc.save();
widget.onLogin?.call();
} else {
msgr.showSnackBar(
diff --git a/flutter/lib/pages/channel_list/channel_list.dart b/flutter/lib/pages/channel_list/channel_list.dart
index 677b524..e54170a 100644
--- a/flutter/lib/pages/channel_list/channel_list.dart
+++ b/flutter/lib/pages/channel_list/channel_list.dart
@@ -42,7 +42,7 @@ class _ChannelRootPageState extends State {
}
try {
- final items = await APIClient.getChannelList(acc.auth!, ChannelSelector.all);
+ final items = (await APIClient.getChannelList(acc.auth!, ChannelSelector.all)).map((p) => p.channel).toList();
items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? ''));
diff --git a/flutter/lib/pages/debug/debug_actions.dart b/flutter/lib/pages/debug/debug_actions.dart
index b4947d5..5573308 100644
--- a/flutter/lib/pages/debug/debug_actions.dart
+++ b/flutter/lib/pages/debug/debug_actions.dart
@@ -17,28 +17,28 @@ class _DebugActionsPageState extends State {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: () => Toaster.success("Hello World", "This was a triumph!"),
child: const Text('Show Success Notification'),
),
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: () => Toaster.info("Hello World", "This was a triumph!"),
child: const Text('Show Info Notification'),
),
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: () => Toaster.warn("Hello World", "This was a triumph!"),
child: const Text('Show Warn Notification'),
),
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: () => Toaster.error("Hello World", "This was a triumph!"),
child: const Text('Show Info Notification'),
),
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: () => Toaster.simple("Hello World"),
child: const Text('Show Simple Notification'),
),
diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart
index 75d219a..c7c8d4b 100644
--- a/flutter/lib/pages/message_list/message_list.dart
+++ b/flutter/lib/pages/message_list/message_list.dart
@@ -48,7 +48,7 @@ class _MessageListPageState extends State {
try {
if (_channels == null) {
final channels = await APIClient.getChannelList(acc.auth!, ChannelSelector.allAny);
- _channels = {for (var v in channels) v.channelID: v};
+ _channels = {for (var v in channels) v.channel.channelID: v.channel};
}
final (npt, newItems) = await APIClient.getMessageList(acc.auth!, thisPageToken, pageSize: _pageSize);
diff --git a/flutter/lib/pages/send/root.dart b/flutter/lib/pages/send/root.dart
index 14f8894..fed2331 100644
--- a/flutter/lib/pages/send/root.dart
+++ b/flutter/lib/pages/send/root.dart
@@ -63,8 +63,8 @@ class _SendRootPageState extends State {
),
),
const SizedBox(height: 16),
- ElevatedButton(
- style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
+ FilledButton(
+ style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: _send,
child: const Text('Send'),
),
@@ -92,7 +92,7 @@ class _SendRootPageState extends State {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}'); //TODO better error display
}
- var url = 'https://simplecloudnotifier.com?preset_user_id=${acc.user!.userID}&preset_user_key=TODO'; // TODO get send-only key
+ var url = 'https://simplecloudnotifier.de?preset_user_id=${acc.user!.userID}&preset_user_key=${acc.auth!.tokenSend}';
return GestureDetector(
onTap: () {
_openWeb(url);
@@ -121,7 +121,7 @@ class _SendRootPageState extends State {
);
}
- var url = 'https://simplecloudnotifier.com?preset_user_id=${acc.user!.userID}&preset_user_key=TODO'; // TODO get send-only key
+ var url = 'https://simplecloudnotifier.de?preset_user_id=${acc.user!.userID}&preset_user_key=${acc.auth!.tokenSend}';
return GestureDetector(
onTap: () {
diff --git a/flutter/lib/state/globals.dart b/flutter/lib/state/globals.dart
index 063ef81..572b16c 100644
--- a/flutter/lib/state/globals.dart
+++ b/flutter/lib/state/globals.dart
@@ -55,4 +55,12 @@ class Globals {
this.sharedPrefs = await SharedPreferences.getInstance();
}
+
+ String? getPrefFCMToken() {
+ return sharedPrefs.getString("fcm.token");
+ }
+
+ Future setPrefFCMToken(String value) {
+ return sharedPrefs.setString("fcm.token", value);
+ }
}
diff --git a/flutter/lib/state/user_account.dart b/flutter/lib/state/user_account.dart
index 8cf8c26..3255e4b 100644
--- a/flutter/lib/state/user_account.dart
+++ b/flutter/lib/state/user_account.dart
@@ -60,10 +60,11 @@ class UserAccount extends ChangeNotifier {
void load() {
final uid = Globals().sharedPrefs.getString('auth.userid');
- final tok = Globals().sharedPrefs.getString('auth.token');
+ final toka = Globals().sharedPrefs.getString('auth.tokenadmin');
+ final toks = Globals().sharedPrefs.getString('auth.tokensend');
- if (uid != null && tok != null) {
- setToken(KeyTokenAuth(userId: uid, token: tok));
+ if (uid != null && toka != null && toks != null) {
+ setToken(KeyTokenAuth(userId: uid, tokenAdmin: toka, tokenSend: toks));
} else {
clearToken();
}
@@ -73,10 +74,12 @@ class UserAccount extends ChangeNotifier {
final prefs = await SharedPreferences.getInstance();
if (_auth == null) {
await prefs.remove('auth.userid');
- await prefs.remove('auth.token');
+ await prefs.remove('auth.tokenadmin');
+ await prefs.remove('auth.tokensend');
} else {
await prefs.setString('auth.userid', _auth!.userId);
- await prefs.setString('auth.token', _auth!.token);
+ await prefs.setString('auth.tokenadmin', _auth!.tokenAdmin);
+ await prefs.setString('auth.tokensend', _auth!.tokenSend);
}
}