Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions lib/app/features/auth/providers/auth_provider.m.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import 'dart:async';

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ion/app/extensions/bool.dart';
import 'package:ion/app/components/message_notification/models/message_notification.f.dart';
import 'package:ion/app/components/message_notification/providers/message_notification_notifier_provider.r.dart';
import 'package:ion/app/extensions/extensions.dart';
import 'package:ion/app/features/auth/providers/local_passkey_creds_provider.r.dart';
import 'package:ion/app/features/core/providers/main_wallet_provider.r.dart';
import 'package:ion/app/features/ion_connect/providers/ion_connect_event_signer_provider.r.dart';
import 'package:ion/app/features/push_notifications/providers/push_subscription_sync_provider.r.dart';
import 'package:ion/app/features/user/providers/biometrics_provider.r.dart';
import 'package:ion/app/features/user/providers/user_metadata_provider.r.dart';
import 'package:ion/app/router/app_routes.gr.dart';
import 'package:ion/app/services/ion_identity/ion_identity_provider.r.dart';
import 'package:ion/app/services/storage/local_storage.r.dart';
import 'package:ion/generated/assets.gen.dart';
import 'package:ion_identity_client/ion_identity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

Expand Down Expand Up @@ -100,6 +106,9 @@ class Auth extends _$Auth {
final authenticatedIdentityKeyNames = state.valueOrNull?.authenticatedIdentityKeyNames ?? [];
if (authenticatedIdentityKeyNames.length > 1) {
ref.read(userSwitchInProgressProvider.notifier).startSwitchingViaLogout();

// Remove push subscription for logged out user
await ref.read(pushSubscriptionSyncProvider.notifier).deletePushSubscriptionForCurrentUser();
}

final ionIdentity = await ref.read(ionIdentityProvider.future);
Expand Down Expand Up @@ -262,6 +271,7 @@ class UserSwitchState with _$UserSwitchState {
const factory UserSwitchState({
@Default(false) bool isSwitchingProgress,
@Default(false) bool isLogoutTriggered,
@Default(false) bool isShowNotification,
}) = _UserSwitchState;
}

Expand All @@ -275,6 +285,10 @@ class UserSwitchInProgress extends _$UserSwitchInProgress {
if (prev != null && next != null && prev != next) {
completeSwitching();
}

if (state.isShowNotification) {
_showPushNotificationAfterSwitching(next);
}
},
);
return const UserSwitchState();
Expand All @@ -291,11 +305,39 @@ class UserSwitchInProgress extends _$UserSwitchInProgress {
);
}

void needToShowPushSwitchNotification() {
state = state.copyWith(
isShowNotification: true,
);
}

void completeSwitching() {
if (state.isLogoutTriggered) {
state = state.copyWith(isLogoutTriggered: false);
} else {
state = const UserSwitchState();
state = state.copyWith(
isSwitchingProgress: false,
isLogoutTriggered: false,
);
}
}

void _showPushNotificationAfterSwitching(String? identityKeyName) {
state = state.copyWith(isShowNotification: false);
if (identityKeyName == null) return;

final userMetadata = ref.read(userMetadataProvider(identityKeyName)).valueOrNull;
final username = userMetadata?.data.name ?? identityKeyName;

final context = rootNavigatorKey.currentContext;
final message =
(context != null && context.mounted) ? context.i18n.switched_to_username(username) : '';

ref.read(messageNotificationNotifierProvider.notifier).show(
MessageNotification(
message: message,
icon: Assets.svg.iconCheckSuccess.icon(size: 16.0.s),
),
);
}
}
40 changes: 40 additions & 0 deletions lib/app/features/core/providers/main_wallet_provider.r.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:ion/app/exceptions/exceptions.dart';
import 'package:ion/app/features/auth/providers/auth_provider.m.dart';
import 'package:ion/app/features/core/providers/wallets_provider.r.dart';
import 'package:ion/app/services/ion_identity/ion_identity_provider.r.dart';
import 'package:ion/app/services/logger/logger.dart';
import 'package:ion/app/services/storage/local_storage.r.dart';
import 'package:ion_identity_client/ion_identity.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

Expand All @@ -28,3 +31,40 @@ Future<Wallet?> mainWallet(Ref ref) async {

return mainWallet;
}

@riverpod
Future<String?> userPubkeyByIdentityKeyName(
Ref ref,
String identityKeyName,
) async {
const cacheKeyPrefix = 'user_pubkey_';

final sharedPrefs = await ref.read(sharedPreferencesFoundationProvider.future);
final cacheKey = '$cacheKeyPrefix$identityKeyName';
final cachedPubkey = await sharedPrefs.getString(cacheKey);

if (cachedPubkey != null && cachedPubkey.isNotEmpty) {
return cachedPubkey;
}

try {
final ionIdentity = await ref.read(ionIdentityProvider.future);
final wallets = await ionIdentity(username: identityKeyName).wallets.getWallets();
final mainWallet = wallets.firstWhereOrNull((Wallet wallet) => wallet.name == 'main');

if (mainWallet != null) {
final userPubkey = mainWallet.signingKey.publicKey;
await sharedPrefs.setString(cacheKey, userPubkey);
return userPubkey;
}
} catch (error, stackTrace) {
Logger.error(
error,
message: 'Failed to get user pubkey for identity key name: $identityKeyName',
stackTrace: stackTrace,
);
return null;
}

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,18 @@ class IonConnectPushDataPayload {
UserMetadataEntity? userMetadata;

if (parsedEvent.kind == IonConnectGiftWrapEntity.kind) {
final result = await unwrapGift(parsedEvent);
decryptedEvent = result.$1;
userMetadata = result.$2;
// Attempt to decrypt GiftWrap message.
// If decryption fails (e.g., message encrypted for another user),
// this is expected - we leave decryptedEvent = null.
// The notification will be shown, but user needs to switch accounts to view content.
try {
final result = await unwrapGift(parsedEvent);
decryptedEvent = result.$1;
userMetadata = result.$2;
} catch (_) {
decryptedEvent = null;
userMetadata = null;
}
}

return IonConnectPushDataPayload._(
Expand Down Expand Up @@ -109,6 +118,10 @@ class IonConnectPushDataPayload {
return false;
}

bool isRecipient(String pubkey) {
return _checkMainEventRelevant(currentPubkey: pubkey);
}

Future<PushNotificationType?> getNotificationType({
required String currentPubkey,
required Future<IonConnectEntity?> Function(EventReference) getRelatedEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,23 @@ class ForegroundMessagesHandler extends _$ForegroundMessagesHandler {
final data = await IonConnectPushDataPayload.fromEncoded(
response.data,
unwrapGift: (eventMassage) async {
final giftUnwrapService = await ref.read(giftUnwrapServiceProvider.future);

final event = await giftUnwrapService.unwrap(eventMassage);
final userMetadata = await ref.read(
userMetadataProvider(event.masterPubkey).future,
);

return (event, userMetadata);
// Attempt to decrypt GiftWrap message.
// If decryption fails (e.g., message encrypted for another user),
// this is expected - we return (null, null) to allow notification display.
// User can switch accounts to view the content.
try {
final giftUnwrapService = await ref.read(giftUnwrapServiceProvider.future);

final event = await giftUnwrapService.unwrap(eventMassage);
final userMetadata = await ref.read(
userMetadataProvider(event.masterPubkey).future,
);

return (event, userMetadata);
} catch (error) {
// Expected: message encrypted for another user
return (null, null);
}
},
);

Expand Down Expand Up @@ -140,16 +149,20 @@ class ForegroundMessagesHandler extends _$ForegroundMessagesHandler {
required IonConnectPushDataPayload data,
}) async {
if (data.event.kind == IonConnectGiftWrapEntity.kind) {
final giftUnwrapService = await ref.watch(giftUnwrapServiceProvider.future);
final currentPubkey = ref.watch(currentPubkeySelectorProvider);

// If no user is logged in, skip the notification
if (currentPubkey == null) {
return true;
}

final rumor = await giftUnwrapService.unwrap(data.event);
if (data.decryptedEvent != null) {
// Skip if message is from current user (self-message)
return data.decryptedEvent!.masterPubkey == currentPubkey;
}

return rumor.masterPubkey == currentPubkey;
// If decryptedEvent is null, message is encrypted for another user - show notification
return false;
}

return false;
Expand Down
Loading