{"record":{"id":"0ca24d4c30e42d3d","repo":"immich-app/immich","slug":"no-token","errorCode":"NO_TOKEN","errorMessage":"No stored change token","messagePattern":"No stored change token","errorType":"error_code","errorClass":"PigeonError","httpStatus":null,"severity":"info","filePath":"mobile/ios/Runner/Sync/MessagesImpl.swift","lineNumber":157,"sourceCode":"  }\n  \n  func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {\n    runSync(completion) { try $0.getMediaChanges() }\n  }\n  \n  private func getMediaChanges() throws -> SyncDelta {\n    guard #available(iOS 16, *) else {\n      throw PigeonError(code: kUnsupportedOSError, message: \"This feature requires iOS 16 or later.\", details: nil)\n    }\n    \n    guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {\n      throw PigeonError(code: \"NO_AUTH\", message: \"No photo library access\", details: nil)\n    }\n    \n    guard let storedToken = getChangeToken() else {\n      // No token exists, definitely need a full sync\n      print(\"MediaManager::getMediaChanges: No token found\")\n      throw PigeonError(code: \"NO_TOKEN\", message: \"No stored change token\", details: nil)\n    }\n    \n    let currentToken = PHPhotoLibrary.shared().currentChangeToken\n    if storedToken == currentToken {\n      return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])\n    }\n    \n    let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)\n    \n    var updatedAssets: Set<AssetWrapper> = []\n    var deletedAssets: Set<String> = []\n    \n    for change in changes {\n      try Task.checkCancellation()\n      guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }\n      \n      let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)\n      deletedAssets.formUnion(details.deletedLocalIdentifiers)","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/mobile/ios/Runner/Sync/MessagesImpl.swift#L139-L175","documentation":"getMediaChanges in MessagesImpl.swift throws PigeonError(code: \"NO_TOKEN\") when no previously persisted PhotoKit change token exists. PhotoKit change tokens let the app fetch only incremental media changes since the last sync; without a stored token the delta cannot be computed, so a full sync is required instead. The error is a signal to the Dart side to fall back to a full media sync and then persist the new token.","triggerScenarios":"Calling getMediaChanges before any full sync has ever stored a change token (e.g. fresh install, app data cleared, or getMediaChanges called before the initial full media listing).","commonSituations":"First launch of the app; after reinstall or 'clear data'; after a bug or migration wiped the stored token from UserDefaults/keychain; calling the incremental sync endpoint from a fresh client that never completed an initial sync.","solutions":["Catch the NO_TOKEN code on the Dart side and perform a full media sync (getMediaItems) instead of an incremental one.","After the full sync succeeds, persist the current PHPhotoLibrary change token via the platform channel so future calls return deltas.","Verify the code that stores the token after a successful sync actually runs (e.g. it is not skipped on background termination)."],"exampleFix":"// before\ntry await syncApi.getMediaChanges();\n// after\ntry {\n  delta = await syncApi.getMediaChanges();\n} on PlatformException catch (e) {\n  if (e.code == 'NO_TOKEN') {\n    delta = await fullSyncAndStoreToken(); // full fetch, then save token\n  } else {\n    rethrow;\n  }\n}","handlingStrategy":"fallback","validationCode":"// Dart: check whether a token has been stored before requesting deltas\nfinal hasToken = await prefs.getBool('hasChangeToken') ?? false;","typeGuard":null,"tryCatchPattern":"try {\n  delta = await api.getMediaChanges();\n} on PlatformException catch (e) {\n  if (e.code == 'NO_TOKEN') delta = await fullSync(); else rethrow;\n}","preventionTips":["Always run a full sync first on fresh installs before incremental syncs.","Persist the change token immediately after every successful sync.","Handle app-data-clear/reinstall by resetting to full-sync mode."],"tags":["photokit","sync","ios","change-token","state-missing"],"backgroundTag":"missing-config-value","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}