Device-side Replay

Re-execute failed requests from the device when the server requests it.

Why Device-Side Replay?

Some requests cannot be replayed from the server because they require resources only available on the user's device:

Setting Up Replay

Configure the onReplayRequest callback during SDK initialization. This callback runs on the device when the server requests a replay:

await EndpointVault.init(
  apiKey: 'your-api-key',
  encryptionKey: 'your-32-char-key',
  onReplayRequest: (eventId, request) async {
    // This runs ON THE DEVICE when server requests replay.
    // We can access device-only resources like:
    // - Fresh auth tokens from secure storage
    // - Biometric verification
    // - Local files for upload
    try {
      // Get a fresh token - the original one has expired
      final token = await authService.getFreshToken();

      final response = await dio.request(
        request.url,
        data: request.requestBody,
        options: Options(
          method: request.method,
          headers: {
            ...?request.requestHeaders?.cast<String, dynamic>(),
            'Authorization': 'Bearer $token', // Fresh token
          },
        ),
      );
      return response;
    } catch (e) {
      return null;
    }
  },
);
How it works: When you click "Retry on Device" in the dashboard, the server sends a replay command. Next time the device syncs with the server, the SDK calls your callback with the original request data.

Handling Post-Replay Operations

Handle any post-replay logic directly in the onReplayRequest callback. A common case is when the API response contains presigned upload URLs — only the device has access to the local files:

await EndpointVault.init(
  apiKey: 'your-api-key',
  encryptionKey: 'your-32-char-key',
  onReplayRequest: (eventId, request) async {
    try {
      // Get fresh auth token from device's secure storage
      final token = await authService.getFreshToken();

      final response = await dio.request(
        request.url,
        data: request.requestBody,
        options: Options(
          method: request.method,
          headers: {
            ...?request.requestHeaders?.cast<String, dynamic>(),
            'Authorization': 'Bearer $token',
          },
        ),
      );

      // Post-replay: upload files using URLs from response
      // Only the device has access to these local files
      if (response.data is Map && response.data['uploadUrls'] != null) {
        final uploadUrls = response.data['uploadUrls'] as List;
        final localFiles = await getLocalFilesForEvent(eventId);
        for (int i = 0; i < uploadUrls.length; i++) {
          await uploadLocalFile(uploadUrls[i], localFiles[i]);
        }
      }

      return response;
    } catch (e) {
      print('Replay failed: $e');
      return null;
    }
  },
);

Complete Example

Here's a full example showing initialization with replay support:

// In main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

  await EndpointVault.init(
    apiKey: 'your-api-key',
    encryptionKey: 'your-32-char-encryption-key!',
    environment: kDebugMode ? 'development' : 'production',

    // This callback runs ON THE DEVICE when server requests replay
    onReplayRequest: (eventId, request) async {
      try {
        // The original auth token has expired by now.
        // Only the device can get a fresh one using the refresh token
        // stored in secure storage (Keychain/Keystore).
        final token = await authService.getFreshToken();

        final response = await dio.request(
          request.url,
          data: request.requestBody,
          options: Options(
            method: request.method,
            headers: {
              ...?request.requestHeaders?.cast<String, dynamic>(),
              'Authorization': 'Bearer $token',
            },
          ),
        );

        // Handle any post-replay workflows that need device resources
        await handleSuccessfulReplay(eventId, response);

        return response;
      } catch (e) {
        print('Replay failed: $e');
        return null;
      }
    },
  );

  // Add the interceptor to your Dio instance
  dio.interceptors.add(EndpointVaultInterceptor());

  runApp(MyApp());
}

Manual Replay (Alternative)

If you prefer manual control, you can poll for replay requests:

// Check periodically or on app resume
Future<void> checkForReplays() async {
  final request = await EndpointVault.instance.checkForReplayRequest();
  if (request != null) {
    final success = await EndpointVault.instance.executeReplay(
      replayRequest: request,
      dio: myDio,
    );
    print('Replay ${success ? 'succeeded' : 'failed'}');
  }
}

How the Flow Works

  1. App captures failure with captureFailure() (automatically via interceptor or manually)
  2. SDK sends encrypted failure data + statistics to server
  3. Server responds with {"replay": {"eventId": "..."}} when it wants a replay
  4. SDK automatically calls your onReplayRequest callback
  5. Your callback re-executes the request and handles any post-replay operations
  6. Return the Response to report success, or null to report failure