touchDevice method

  1. @override
Future<Either<RepositoryFailure, Unit>> touchDevice()
override

Marks this device as active by updating lastActiveAt.

Updates the device document's lastActiveAt timestamp to indicate recent activity. Used by backend to determine:

  • Which devices are "active" for notification delivery
  • Which devices to clean up as stale

Throttling

Default throttle is 60 minutes to avoid excessive writes. Configurable via Remote Config (dreamic_device_touch_throttle_minutes).

When Called

  • On app resume from background (throttled)
  • After registerDevice (implicitly updates lastActiveAt)

Server Behavior

Uses upsert semantics: if the device doc doesn't exist, it will be created with minimal fields (deviceId, lastActiveAt, updatedAt).

Returns

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

Implementation

@override
Future<Either<RepositoryFailure, Unit>> touchDevice() async {
  logd('DeviceService: touchDevice called');

  try {
    final now = DateTime.now();
    final throttleMinutes = AppConfigBase.deviceTouchThrottleMinutes;

    // Apply throttle
    if (_lastTouchAt != null &&
        now.difference(_lastTouchAt!) < Duration(minutes: throttleMinutes)) {
      logd('DeviceService: Touch throttled, last touch was ${now.difference(_lastTouchAt!).inMinutes} minutes ago');
      // Try to flush any pending payload on this lifecycle event (if authenticated)
      if (_isUserAuthenticated()) {
        await _flushPendingPayload();
      }
      return const Right(unit);
    }

    final deviceId = await getDeviceId();

    // Check authentication - if not authenticated, store pending and return
    if (!_isUserAuthenticated()) {
      logd('DeviceService: User not authenticated, storing pending touch');
      await _updatePendingPayload(
        deviceId: deviceId,
        touch: true,
      );
      // Return success since we've stored it for later
      return const Right(unit);
    }

    logd('DeviceService: Touching device $deviceId');

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

    final data = Map<String, dynamic>.from(result.data as Map);
    if (data['success'] != true) {
      logw('DeviceService: Touch response indicated failure');
      // Store touch in pending payload for retry
      await _updatePendingPayload(
        deviceId: deviceId,
        touch: true,
      );
      return const Left(RepositoryFailure.unexpected);
    }

    _lastTouchAt = now;

    // Try to flush any other pending data now that we have connectivity
    await _flushPendingPayload();

    logd('DeviceService: Device touched successfully');
    return const Right(unit);
  } on FirebaseFunctionsException catch (e) {
    loge(e, 'DeviceService: Firebase Functions error during touch');

    // Store touch in pending payload for retry on transient errors
    if (_shouldStorePendingOnError(e)) {
      final deviceId = await getDeviceId();
      await _updatePendingPayload(
        deviceId: deviceId,
        touch: true,
      );
    }

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

    // Store touch in pending payload for retry
    final deviceId = await getDeviceId();
    await _updatePendingPayload(
      deviceId: deviceId,
      touch: true,
    );

    return const Left(RepositoryFailure.unexpected);
  }
}