persistFcmToken method

  1. @override
Future<Either<RepositoryFailure, Unit>> persistFcmToken({
  1. required String? fcmToken,
})
override

Persists the FCM token to the backend device record.

Called by NotificationService when it obtains/refreshes the token or clears it due to notifications being disabled. This method only handles backend persistence—NotificationService owns the token lifecycle (fetch/refresh/cache/local state).

When Called

  • Initial token acquisition (first successful read after enabling)
  • Token rotation/refresh (Firebase Messaging token refresh event)
  • Token cleared due to local disablement (user disables notifications in-app while staying logged in)

Important: Logout Path

Do NOT call this method during logout. DeviceService deletes the device doc on logout via unregisterDevice; NotificationService should perform only local cleanup (clear cached token, detach listeners) without triggering a backend write that would race with device doc deletion.

Token Uniqueness

The backend enforces that a token appears on at most one device doc. On token update, it clears the same token from any other device docs (handles edge cases from offline failures or account switching).

Parameters

  • fcmToken: The new FCM token, or null to clear the token.

Returns

  • Right(unit) on success
  • Left(RepositoryFailure) on failure

Example

// Token obtained
await deviceService.persistFcmToken(fcmToken: newToken);

// Token cleared (e.g., user revoked permission while logged in)
await deviceService.persistFcmToken(fcmToken: null);

Implementation

@override
Future<Either<RepositoryFailure, Unit>> persistFcmToken({
  required String? fcmToken,
}) async {
  logd('DeviceService: persistFcmToken called with token: ${fcmToken != null ? '***' : 'null'}');

  try {
    final deviceId = await getDeviceId();

    // Check authentication - if not authenticated, store pending and return
    if (!_isUserAuthenticated()) {
      logd('DeviceService: User not authenticated, storing pending token update');
      await _updatePendingPayload(
        deviceId: deviceId,
        fcmToken: fcmToken ?? '', // Empty string = explicit null
        hasChangedFields: true,
      );
      // Return success since we've stored it for later
      return const Right(unit);
    }

    final result = await _deviceCallable.call({
      'action': 'updateToken',
      'deviceId': deviceId,
      'fcmToken': fcmToken,
    });

    final data = Map<String, dynamic>.from(result.data as Map);
    if (data['success'] != true) {
      logw('DeviceService: persistFcmToken response indicated failure');
      // Store token update in pending payload
      // Use empty string as sentinel for explicit null
      await _updatePendingPayload(
        deviceId: deviceId,
        fcmToken: fcmToken ?? '', // Empty string = explicit null
        hasChangedFields: true,
      );
      return const Left(RepositoryFailure.unexpected);
    }

    // Clear any pending token update since we just synced
    // Note: We don't clear the entire pending payload, just mark that
    // token updates don't need to be re-sent. However, since our merge
    // is last-write-wins, a successful sync here is good enough - the
    // next flush will use the most recent token value anyway.

    logd('DeviceService: FCM token updated successfully');
    return const Right(unit);
  } on FirebaseFunctionsException catch (e) {
    loge(e, 'DeviceService: Firebase Functions error during token update');

    // Store token update in pending payload for retry on transient errors
    if (_shouldStorePendingOnError(e)) {
      final deviceId = await getDeviceId();
      await _updatePendingPayload(
        deviceId: deviceId,
        fcmToken: fcmToken ?? '', // Empty string = explicit null
        hasChangedFields: true,
      );
    }

    return _mapFirebaseFunctionsException(e);
  } catch (e) {
    loge(e, 'DeviceService: Unexpected error during token update');

    // Store token update in pending payload for retry
    final deviceId = await getDeviceId();
    await _updatePendingPayload(
      deviceId: deviceId,
      fcmToken: fcmToken ?? '', // Empty string = explicit null
      hasChangedFields: true,
    );

    return const Left(RepositoryFailure.unexpected);
  }
}