FCM kinda works [does not receive notifications]

This commit is contained in:
Mike Schwörer 2024-06-01 03:06:02 +02:00
parent 4c02afb957
commit 0b7fb533da
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
15 changed files with 219 additions and 68 deletions

View File

@ -2,6 +2,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.android.vending.BILLING" />
<application
android:label="simplecloudnotifier"

View File

@ -45,13 +45,15 @@ class APIClient {
final req = http.Request(method, uri);
print('[REQUEST|RUN] [${method}] ${name}');
if (jsonBody != null) {
req.body = jsonEncode(jsonBody);
req.headers['Content-Type'] = 'application/json';
}
if (auth != null) {
req.headers['Authorization'] = 'SCN ${auth.token}';
req.headers['Authorization'] = 'SCN ${auth.tokenAdmin}';
}
req.headers['User-Agent'] = 'simplecloudnotifier/flutter/${Globals().platform.replaceAll(' ', '_')} ${Globals().version}+${Globals().buildNumber}';
@ -98,9 +100,11 @@ class APIClient {
if (fn != null) {
final result = fn(data as Map<String, dynamic>);
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<Client> addClient(KeyTokenAuth? auth, String fcmToken, String agentModel, String agentVersion, String clientType) async {
static Future<Client> 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<Client> updateClient(KeyTokenAuth? auth, String clientID, String fcmToken, String agentModel, String agentVersion) async {
static Future<Client> 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<UserWithClientsAndKeys> 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,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Client> 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<String, dynamic> json) {
return UserWithClientsAndKeys(
user: User.fromJson(json),
clients: Client.fromJsonArray(json['clients'] as List<dynamic>),
sendKey: json['send_key'] as String,
readKey: json['read_key'] as String,
adminKey: json['admin_key'] as String,
);
}
}

View File

@ -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<AccountRootPage> {
late UserAccount userAcc;
bool loading = false;
@override
void initState() {
super.initState();
@ -102,25 +110,54 @@ class _AccountRootPageState extends State<AccountRootPage> {
}
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<AccountLoginPage>(builder: (context) => AccountLoginPage()));
},
child: const Text('Use existing account'),
),
],
),
);
@ -391,4 +428,40 @@ class _AccountRootPageState extends State<AccountRootPage> {
),
);
}
void createNewAccount() async {
setState(() => loading = true);
final acc = Provider.of<UserAccount>(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);
}
}
}

View File

@ -54,8 +54,8 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
),
),
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<AccountLoginPage> {
void _login() async {
final msgr = ScaffoldMessenger.of(context);
final prov = Provider.of<UserAccount>(context, listen: false);
final acc = Provider.of<UserAccount>(context, listen: false);
try {
final uid = _ctrlUserID.text;
@ -79,8 +79,8 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
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(

View File

@ -42,7 +42,7 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
}
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 ?? ''));

View File

@ -17,28 +17,28 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
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'),
),

View File

@ -48,7 +48,7 @@ class _MessageListPageState extends State<MessageListPage> {
try {
if (_channels == null) {
final channels = await APIClient.getChannelList(acc.auth!, ChannelSelector.allAny);
_channels = <String, ChannelWithSubscription>{for (var v in channels) v.channelID: v};
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
}
final (npt, newItems) = await APIClient.getMessageList(acc.auth!, thisPageToken, pageSize: _pageSize);

View File

@ -63,8 +63,8 @@ class _SendRootPageState extends State<SendRootPage> {
),
),
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<SendRootPage> {
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<SendRootPage> {
);
}
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: () {

View File

@ -55,4 +55,12 @@ class Globals {
this.sharedPrefs = await SharedPreferences.getInstance();
}
String? getPrefFCMToken() {
return sharedPrefs.getString("fcm.token");
}
Future<bool> setPrefFCMToken(String value) {
return sharedPrefs.setString("fcm.token", value);
}
}

View File

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