Repository navigation
feat(ai): add AIClientIdentity.installId for a per-install client id - #43
Conversation
`AIClientIdentity.installId()` creates `flutter-<UUID>` on first use and keeps it in shared preferences under `io.getstream.ai.client-id`, for passing as `clientId`. On Android the id is in a preferences file of its own, so an app's backup rules can exclude it; the README has the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AIClientIdentity.installId for a per-install client id
The previous lockfile was resolved with an older Flutter SDK, which downgraded the packages it pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xsahil03x
left a comment
There was a problem hiding this comment.
Thanks! Matching iOS and Android's installId closes a real gap for client tools on Flutter. I ran it on an Android emulator: the id lands in shared_prefs/io.getstream.ai.client-id.xml and is the same after a force-stop and relaunch. Analyze and tests pass, and the example builds for web and macOS.
Please do this before merging:
- On Android the id is backed up unless every app adds the README's rules. The default Flutter template leaves backup on; the example app reports
ALLOW_BACKUPand has no rules. A restore or device transfer then gives two phones the sameclientId, and both run a call meant for one, which is the case this API exists to prevent. Native Android keeps the id innoBackupFilesDir(AIClientToolRunner.kt:219-227), which is safe with no app config. Could we store it underno_backuptoo, e.g. as a file next togetApplicationSupportDirectory()'s parent, which isfilesDiron Android? If that's not an option, the backup warning should open the dartdoc and the CHANGELOG entry.
The rest is optional and inline.
Agreed, It adds |
- `AIClientIdentity.installId()` creates a new id when the stored one is empty or blank, instead of returning it forever. - The dartdoc and README no longer describe the id's format or storage key, and say that `SharedPreferencesAsync().clear()` removes the id on iOS, macOS, web and desktop, and that iOS backups copy it. - The README explains the `StateError` widget tests hit and the in-memory store that fixes it. - `installId()` gets its own CHANGELOG entry. - Tests cover a failed write, and check that a retried id is stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yes to
That drops Could you keep the read/create logic behind a small internal store class rather than inside It does stop sharing the id with native iOS, which keeps it in |
…-7464aa # Conflicts: # packages/stream_chat_flutter_ai/pubspec.yaml # pubspec.lock # pubspec.yaml
`AIClientIdentity.installId()` no longer uses shared preferences. The id now lives in a file named `io.getstream.ai.client-id`: - Android: in `no_backup`, the same file the native Android SDK uses, so backups and device transfers skip it with no app setup. - iOS and macOS: in Application Support, marked excluded from backup. - Windows and Linux: in Application Support. - Web: in `localStorage`. The read/create logic lives in an internal `AIInstallIdStore`. Two engines creating the id at once go through an exclusive file create, so they end up with one id. The README's Android backup rules are gone, and its widget-test section now recommends a fixed `clientId`, or `tester.runAsync` with a `path_provider` fake. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Done in 30564db, following your outline:
As you noted, iOS no longer shares the id with the native SDK. That's fine, since an app embedding both is very unlikely. |
xsahil03x
left a comment
There was a problem hiding this comment.
Thanks! One file-based store everywhere is a clean result. I tried it on an Android emulator (the id lands in no_backup, survives a force-stop, picks up an id written by the native SDK, recovers from an empty file, and pm clear resets it) and on macOS (in the sandbox container, with tmutil isexcluded reporting the file excluded). Android, iOS, macOS, web and web --wasm all build, the web store passes in Chrome under both compilers, and analyze and all tests pass.
Please do these before merging:
- Don't let a failed backup exclusion fail
installId(): today one failure makes every later call fail although the id is on disk (inline onai_install_id_storage_io.dart:80and:96). - Uninstalling only removes the id on Android and iOS; the dartdoc and README say it does everywhere (inline).
- The storage interface promises the first engine's id wins, which isn't true once the checks give up (inline).
The rest is optional and inline.
- A failed backup exclusion no longer fails `read` or `create`: the id is stored either way, and the next read tries the exclusion again. - The dartdoc and README say uninstalling removes the id only on Android and iOS, and the storage interface says when a racing id can be replaced. - Name the wait between checks for another engine's write, give every skipped test a reason, mark the install id types `@internal`, and make the README's widget-test snippet restore what it changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xsahil03x
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround! A failed backup exclusion no longer fails read or create, and the two new tests fail against the previous commit and pass now. The uninstall wording and the create contract match what the code does, and the @internal markers stay off everything exported. Analyze, format and all tests pass, and the example still builds for web (JS and Wasm), Android and macOS.
Linear: FLU-
Description of the pull request
Adds
AIClientIdentity.installId(), an id for this install of the app to pass asclientId. iOS (AIClientIdentity.installID) and Android (AIClientIdentity.installId(context)) already ship this. Apps such as the Vision Agents Flutter demo can now address client tool calls to one install the same way.flutter-followed by an uppercase UUID), created on first use and kept across launches. Calls that race the first one get the same id. A blank stored value counts as missing. If the id can't be read or stored, the call fails and the next call tries again.io.getstream.ai.client-id:no_backup(the parent ofgetApplicationSupportDirectory(), which isfilesDir, plusno_backup). This is the same file the native Android SDK uses, so an app that embeds both gets one id.NSURL.setResourceValuethroughpackage:objective_c.localStorage, through a conditional import.UserDefaults, which backups restore, so the Flutter id is no longer shared with native iOS. That's a deliberate tradeoff, since an app embedding both SDKs is very unlikely.File.create(exclusive: true). Only one writes the id, and the other reads it back.AIInstallIdStore, not inAIClientIdentity, so it can move tostream_core_flutterlater as is.android/orios/folder and no native code.path_provider: ^2.1.5(whichstream_chat_flutterandstream_chat_persistencealready use),objective_c: ^9.3.0,web: ^1.1.0anduuid: ^4.5.1.objective_candwebwere already in the lock through other dependencies.path_provider_platform_interfaceandplugin_platform_interfaceare dev dependencies for the tests.client_idon every message. For widget tests, they recommend a fixedclientId, ortester.runAsyncwith apath_providerfake.Testing
xattr)./data/user/0/<pkg>/no_backup/io.getstream.ai.client-id, readable only by the app, and the same id comes back after a force-stop and relaunch. A fulladb backupincludes a control file fromfiles/but nothing fromno_backup.Library/Application Support, carries the backup-exclusion attribute, and comes back unchanged after two relaunches.tmutil isexcludedreports the file as excluded.localStorage. An existing id is reused, a blank one is replaced, and an id another tab stored first wins.🤖 Generated with Claude Code