# Change Log

## 2026-04-22 — Bug Hunt Round 20 (Card feature pre-submission — 4 fixes)

**CardManager.swift** (R20-A01 Critical)
- `saveDraft()`: strip `editToken` to `""` before JSON-encoding and writing to UserDefaults. Previously the secret token was persisted in plaintext in the app's Documents/UserDefaults plist, which would be included in device backups (iCloud / iTunes / Time Machine). Only Keychain should hold the token.

**CardKeychain.swift** (R20-A02)
- `save(username:editToken:)`: skip `set(tokenAccount, value: editToken)` when editToken is empty. Previously an empty token from a transient `resolveEditToken` failure would overwrite the last known-good token in the Keychain, breaking subsequent edits until the user manually re-synced.

**CardModel.swift** / **CardWriteView.swift** (R20-A03)
- `publicURL`: returns `""` instead of `"https://.nfc.bz"` when username is empty, so share/NFC write callers can bail out. `CardWriteView.writeToTag` now guards on empty URL before calling `writer.write`. Previously, an edge-case empty-username card would write an invalid URL to the tag.

**CardEditorView.swift** / **CardUsernamePickerView.swift** / **Localizable.xcstrings** (R20-B01)
- Replaced hardcoded `TextField("username", …)` placeholder with localization key `card.username.placeholder`. Added en (`"username"`) + ja (`"ユーザー名"`) translations.

## 2026-04-22 — Bug Hunt Round 19 (5-Agent: NFC/Auth/CardUI/StoreKit/History — 4 fixes)

**NFCWriter.swift** (R19-A01 Critical)
- `writeToTag`: changed callback to `[weak self]` + `guard let self else { session.invalidate(); return }`. Previously the callback held a strong implicit self — if the view was dismissed while a write was in flight, the callback would execute on a deallocated object.
- `didInvalidateWithError`: added `[weak self]` + guard to `DispatchQueue.main.async` block. Same pattern as above — invalidation callbacks can arrive after NFCWriter deallocates.

**HistoryView.swift** (R19-B01 Critical)
- `.onDelete`: snapshot `visibleItems` into a local `let snapshot` before passing to `delete(ids:)`. Previously `visibleItems` was read inline inside the closure, which is a TOCTOU race — an iCloud merge notification arriving between swipe and delete commit could shift indices and delete wrong items.

**AnalysisView.swift** (R19-C01 Critical)
- `RecordFixSheet`: added `@EnvironmentObject var history: HistoryManager`. On write success `onChange`, now calls `history.add(url: finalValue, type: .write)` before `onWriteSuccess()`. Previously, writes from the tag-fix sheet were invisible in history and the widget.

**WriteView.swift** (R19-D01)
- Added `@State private var lastWrittenURL`. Snapshot the URL just before `nfcWriter.write(url:)` is called. History now records the snapshotted URL, not the current `urlText` at the time the success message arrives (which could differ if the user edited the field while NFC was scanning).

## 2026-04-22 — Bug Hunt Round 18 (5-Agent: NFC/StoreKit/TeamMode/ToolForm/Widget — 6 fixes)

**ToolFormView.swift** (R18-C01 Critical)
- `performReviewWrite()`: added `guard !isCreatingTag else { return }` at function entry. Double-tap could consume two review credits before NFC session started, because `isCreatingTag` was set after the guard checks but still within the same sync frame.

**NFCReader.swift** (R18-A01)
- `read()`: added `session?.invalidate(); session = nil` before creating a new `NFCNDEFReaderSession`. Double-tap could start two concurrent CoreNFC sessions, violating the single-session contract.

**TeamManager.swift** (R18-D01)
- `syncWithServer()`: added `syncInFlight` guard (mirrors CardManager pattern). Rapid foreground-entry notifications could spawn concurrent sync calls, causing interleaved `members` array mutations.

**ToolFormView.swift** (R18-C02)
- Removed empty `onChange(of: nfcWriter.isWriting || nfcWriter.isShortening)` block. Dead code triggered unnecessary SwiftUI reactivity on every write-state change.

**ToolFormHelpers.swift** (R18-B01)
- `geocodeAddress()`: added stale-result guard in completion block — checks `input1` hasn't changed before applying geocode result. Rapid address input previously allowed earlier slower geocode responses to overwrite the current correct result.

**SettingsView.swift** / **Localizable.xcstrings** (R18-D02 Critical)
- Replaced hardcoded Japanese strings `"サインインして機能を有効化"`, `"分析・タグ管理・デバイス間同期"`, and `"管理ダッシュボード"` with localization keys `settings.account.signInPromptTitle`, `settings.account.signInPromptSubtitle`, and `settings.admin.dashboard`. Added en + ja translations to Localizable.xcstrings.

## 2026-04-22 — Bug Hunt Round 17 (5-Agent: Concurrency/UX/Architecture/Edge Cases/Integration — 16 fixes)

**WriteEventUploader.swift** (R17-A21 Critical)
- `flushQueue`: snapshot queue before loop; track `failedIDs`; only remove events whose upload succeeded. Previously the queue was drained upfront, causing permanent event loss on network failure.

**NFCWriter.swift** (R17-A22)
- `verifyAndFinish` Task: added `guard !Task.isCancelled else { session.invalidate(); return }` after 300ms sleep. Prevents writes to sessions already invalidated by `cancelSession()` or scenePhase background.

**AuthManager.swift** (R17-A23)
- `Keychain.setData`: added `#if DEBUG` log when `SecItemAdd` returns non-zero status. Previously all Keychain write failures were silent.

**CardManager.swift** (R17-A24, R17-D14)
- `update()`: added `guard !isBusy` to prevent concurrent mutations overwriting each other's snapshots.
- `refreshCard()`: when server returns nil (card deleted), clears `self.card`, Keychain, UserDefaults, and KVS — transitions to NoCardView instead of showing stale ghost card.

**CardEditorView.swift** (R17-B15)
- `lastNameKanaUserEdited` / `firstNameKanaUserEdited`: added `onChange` on kanji fields to reset flag when field is cleared. Previously clearing the kanji field left the kana flag locked, permanently disabling auto-fill.

**ToolFormComponents.swift** (R17-B17)
- `PhoneFormField`: removed `.onAppear { showKeyboard = true }`. Keyboard was re-triggered on every tab switch back to a form containing a phone field.

**CardPreviewView.swift** (R17-B19)
- `ForEach` on contact and social rows: changed `id: \.label` to `id: \.offset` (via `enumerated()`). Duplicate label strings (e.g. two websites) caused SwiftUI identity collision and potential crash.

**SettingsView.swift** (R17-B21)
- Sign-in prompt: added `@State private var hasShownSignInPrompt = false` guard. Prompt now fires at most once per session instead of on every tab re-selection.

**TeamManager.swift** (R17-C13, R17-C21, R17-D18)
- `removeMember`: added `guard !isBusy` + `isBusy = true` / `defer isBusy = false`. Double-tap could delete the same member twice.
- `addMember`: added pre-flight member limit check via `memberLimit` parameter. Server call no longer attempted when limit already reached.
- `syncWithServer`: catches `CardClientError.notFound` specifically and calls `reset()` to clear ghost team UI when the team is deleted server-side.

**UserDefaultsKeys.swift** / **NFCTagLabelSync.swift** (R17-C15)
- Added `UDKey.labelSyncLastRun = "label_sync_last_run_at"`. Updated `NFCTagLabelSync` to use `UDKey.labelSyncLastRun` instead of the hardcoded string.

**StoreManager.swift** (R17-D21)
- `refreshEntitlements()`: added trial elapsed-time re-evaluation. Previously `isTrialActive` was computed only in `init()` — a trial that expired mid-session would not flip `hasProAccess` to false until next launch.

**NFCCopier.swift** (R17-D22)
- Empty source tag path: moved `self.state = .failed(...)` to before `session.invalidate()` (via `DispatchQueue.main.async`). UI now reflects the error before the NFC sheet dismisses.

**OCRScannerView.swift** (R17-D16)
- `dataScanner(_:didTapOn:)`: added `guard !recognized.isEmpty else { return }` before dismissing scanner. Empty OCR transcripts no longer wipe the bound text field.

## 2026-04-22 — Bug Hunt Round 16 (5-Agent: Runtime/UX/Design/QA/Integration — 11 fixes)

**CardModel.swift** (R16-C04)
- Added `MyCard.encodeForAPI()` as single canonical encoder for all API clients. Both `SupabaseCardClient.encodeForInsert` and `TeamClient.encodeCard` now delegate to this method. Eliminates field-drift risk when new `MyCard` properties are added.

**StoreManager.swift** (R16-A18)
- `syncTransactionToNfcBz` catch block: demoted error from `lastErrorMessage = ...` to `#if DEBUG print(...)`. Background sync failure no longer surfaces as a purchase-failure alert after a successful IAP.

**NFCWriter.swift** (R16-A16)
- `verifyAndFinish`: captured `isClearOperation` into `let wasClear` and reset it before the 300ms `Task.sleep`. Eliminates data race where a concurrent `didInvalidateWithError` reset could corrupt the flag read inside the CoreNFC callback.

**WriteView.swift** / **SearchWriteView.swift** (R16-D03)
- Added `@Environment(\.scenePhase)` and `onChange(of: scenePhase) { phase == .background { nfcWriter.cancelSession() } }`. Matches the existing `ToolFormView` pattern. Prevents `isWriting` stuck true when iOS silently drops the NFC session on app background.

**SimpleWriteView.swift** (R16-B05)
- History dropdown rewrite button: added `.disabled(nfcWriter.isWriting || nfcWriter.isShortening)` and gray background while writing. Prevents concurrent NFC session from history during an active write.

**PaywallView.swift** (R16-B07, R16-B13)
- `proLoadingSpinner`: final `else` branch replaced `ProgressView()` with an error icon (`exclamationmark.triangle`). Covers the "StoreKit loaded but product list empty" state that previously showed an infinite spinner.
- `CreditPackCard` overlay stroke: changed from `Theme.accent` (same as filled background) to `Color.white.opacity(0.5)` when selected. Selection ring is now visible.

**HistoryView.swift** (R16-B10)
- Trash toolbar button guard: `!history.items.isEmpty` → `!visibleItems.isEmpty`. Button no longer appears when only tap history exists with an empty write list.

**CardManager.swift** (R16-D01)
- `suggestUsername(maxAttempts:)`: changed `for i in 2...maxAttempts` to `for i in 2...max(2, maxAttempts)`. Prevents fatal `ClosedRange` crash when caller passes `maxAttempts < 2`.

**UserDefaultsKeys.swift** + **ToolFormView.swift** + **ToolFormHelpers.swift** (R16-C01, R16-C02)
- Added `UDKey.reviewSearchHistory`, `UDKey.shortcutNameHistory`, `UDKey.toolFormStorage(_:)`.
- Changed `ToolFormView.reviewHistoryKey` and `shortcutHistoryKey` from bare strings / instance `let` to `static let` referencing the enum constants.
- All call sites updated to `Self.shortcutHistoryKey` / `UDKey.*`.

**SupabaseCardClient.swift** (R16-C05)
- `fetchEditToken`: replaced hardcoded `"https://krbkqkqpxxjdboqxfhyj.supabase.co/functions/v1/card-claim"` with `SupabaseClient.url.absoluteString + "/functions/v1/card-claim"`. Single source of truth for the Supabase project URL.

**CardModeRootView.swift** (R16-A20)
- `create()`: removed redundant `saving = true` inside the async function body. The caller's pre-Task `saving = true` is the canonical guard; `defer { saving = false }` handles cleanup.

**CardUsernamePickerView.swift** (R16-D12)
- Added `CheckState.networkError` case. When `checkUsernameAvailable` returns `nil` (offline), the picker now shows a "Cannot verify" icon and allows the user to proceed (server validates uniqueness on insert with a 409 response). Previously the user was silently blocked with no explanation.

**ToolFormHelpers.swift** (R16-C08)
- Removed `_ = item` no-op suppression. Changed `var item` to `let item` to clarify that the value is not mutated after construction.

## 2026-04-22 — Bug Hunt Round 15 (5-Agent Deep Audit: NFC/Async/Memory)

### NFCTool iOS — R15: 11 fixes across retain cycles, session safety, async cancellation

**NFCAnalyzer.swift + NFCWriter.swift** (R15-01)
- NFC delegate closures (`session.connect`, `tag.queryNDEFStatus`, `tag.readNDEF`) changed to `[weak self]` capture with `guard let self` fallback. Prevents retain cycle between `NFCNDEFReaderSession` and its delegate that prevented deallocation across repeated scans.

**NFCWriter.swift** (R15-02, R15-14)
- `verifyAndFinish`: replaced `DispatchQueue.global().asyncAfter` with `Task { [weak self] in try? await Task.sleep(for: .milliseconds(300)) }`. Eliminates @Published mutation from background thread.
- `cancelSession()`: now resets `sessionAlertMessage` to default. Prevents "Hold near tag to lock" prompt appearing on the next URL write session.

**CardModeRootView.swift** (R15-03)
- `CreateCardFlow`: added `.onDisappear { checkTask?.cancel() }`. Username availability check no longer runs after the view is dismissed.

**TeamMemberDetailView.swift** (R15-05)
- `openEditInBrowser()`: added `guard !card.editToken.isEmpty` before constructing URL. Prevents opening Safari with an auth-less `?t=` URL.

**NFCCopier.swift** (R15-08)
- `startWritingDestination()`: added `session?.invalidate(); session = nil` before creating the new session. Prevents two concurrent NFC sessions on rapid Retry taps.

**StoreManager.swift** (R15-10)
- Pro/lifetime purchase path: 3 bonus credits now gated behind dedup check using `UDKey.grantedTxIDs` with `pro_bonus_` prefixed key. Prevents double-grant on sandbox restore or re-delivery.

**PaywallView.swift** (R15-11)
- Added `.onChange(of: store.isLiveTagSubscriber)` and `.onChange(of: store.isTeam)` dismiss triggers alongside existing `isPro` trigger.

**HistoryManager.swift** (R15-12)
- Extracted `DateFormatter` from inline closure in `save()` to `lazy var widgetDateFormatter`. Eliminates repeated formatter allocation on every save call.

**FriendCardView.swift** (R15-15)
- `writeSingle(_:)`: added `guard !(nfcWriter.isWriting || nfcWriter.isShortening)` at entry. Menu items can no longer bypass the `.disabled()` modifier to open a second NFC session.

**SearchWriteView.swift** (R15-17)
- Added `@State private var searchTask: Task<Void, Never>?`. `runSearch()` stores the task and cancels the previous one. `.onDisappear { searchTask?.cancel() }` prevents stale results from being applied after dismiss.

**CardManager.swift** (R15-18)
- `kvsObserver: NSObjectProtocol?` property added. `NotificationCenter.addObserver(forName:…)` result now stored. `deinit` calls `removeObserver(kvsObserver)`. Prevents dangling observer accumulation.

### Side-effect checks
- `NFCAnalyzer` readNDEF closure: added `guard let self` before existing `self.` references — all paths handled.
- `NFCWriter.verifyAndFinish` Task: `[weak self]` checked for all inner closures including `lockTag` call path.
- `StoreManager` Pro bonus dedup: prefix `pro_bonus_` ensures no collision with consumable transaction IDs in same array.
- `HistoryManager.widgetDateFormatter`: `lazy var` on `@MainActor` class — thread-safe by actor isolation.
- `SearchWriteView.searchTask` cancel: `guard !Task.isCancelled` added before both `results` and `errorText` mutations.

## 2026-04-22 — Bug Hunt Round 14 (5-Agent Deep Audit)

### NFCTool iOS — R14: 9 fixes across security/runtime/UX/maintainability

**ContentView.swift** (R14-02)
- Removed inline admin email array from `.onChange(of: auth.user?.email)`. Now uses `auth.isAdmin` from `AdminManager` as single source of truth.

**ToolFormView.swift** (R14-03, R14-20)
- Removed dead `if false && !previewURI.isEmpty && isValid` preview block (26 lines of dead code).
- Simplified `.foregroundStyle(store.reviewCredits > 0 ? .white : .white)` to `.foregroundStyle(.white)` (dead ternary).

**StoreManager.swift** (R14-11, R14-26)
- Fixed `observeTransactionUpdates()` credit path: `await transaction.finish()` now called AFTER `addCredits()` (consistent with `purchase()` path). Prevents potential credit loss on process kill between finish and addCredits.
- Fixed credit dedup set trimming in both `purchase()` and `observeTransactionUpdates()`: changed from non-deterministic `Set→Array.suffix` to ordered `[String]` array with `append+suffix`. Prevents old transaction IDs from being preserved over recent ones.

**SettingsView.swift** (R14-12, R14-19)
- Wrapped debug diagnostic string in `labelSyncResultMessage` with `#if DEBUG`. Production users no longer see `"認証:true token:true"` in the label sync alert.
- Removed two `UserDefaults.standard.synchronize()` calls (deprecated since iOS 12, unnecessary).

**AnalysisView.swift** (R14-13, R14-23)
- Removed `onAppear` auto-trigger of `analyzer.analyze()`. NFC scanning now requires explicit user tap (the existing Analyze button).
- Fixed `navigationTitle` from `"sns.navigationTitle"` to `"analysis.navigationTitle"`.

**CardModeRootView.swift** (R14-09)
- `CreateCardFlow`: `saving = true` now set synchronously in button action before `Task { await create() }`, eliminating the rapid double-tap window.

**WriteEventUploader.swift** (R14-22)
- Changed `enqueue()` from fire-then-queue-on-failure to queue-first-then-upload. Added `removeFromQueue(_ clientEventId:)` helper. Process kill during upload no longer loses the write event.

**HistoryView.swift** (R14-24)
- `visibleItems` now filters to `.write` type items only, matching the `"history.section.writes"` section header. Read-type items no longer appear in the writes section.

### Side-effect checks
- `StoreManager` dedup: both `purchase()` and `observeTransactionUpdates()` now use same ordered-array pattern. Verified no third dedup site exists.
- `WriteEventUploader.flushQueue()`: uses `pendingQueue` directly; compatible with new remove-on-success pattern since both are `@MainActor`.
- `AnalysisView` auto-scan removal: `RecordFixSheet.onWriteSuccess` still calls `analyzer.analyze()` (intentional re-scan after fix).
- `HistoryView` write filter: `history.section.taps` section unaffected (uses separate `TapHistoryView`).

## 2026-04-21 — Bug Hunt Round 11 (Multi-Agent Full Scan)

### NFCTool iOS — R11: navigation titles, i18n, safety, continuation leak

**ToolsView.swift / WiFiSavedListView.swift / WriteView.swift**
- B-14/B-15/B-16: `.navigationTitle("sns.navigationTitle")` → 各画面の正しいキーに修正

**BusinessRootView.swift**
- B-17: `.navigationTitle` → `"mode.business.title"` キー化
- B-03: placeholder CompanyEntry `"公式サイト"` → `"business.entry.defaultName"` ローカライズキー化

**CompanyWriteView.swift**
- B-18: `.navigationTitle("sns.navigationTitle")` → `"mode.business.title"` キー化

**PaywallView.swift**
- B-13: `Text("NFC SNS CARD MAKER Pro")` → `Text("paywall.hero.title")` ローカライズキー化

**ToolFormHelpers.swift**
- C-02: `URLComponents(string:)!` force unwrap → safe guard + fallback

**CardMode/CardEditorView.swift**
- D-05: `withCheckedContinuation` → `withTaskCancellationHandler { await withCheckedContinuation {...} }` でキャンセル時のリーク防止
- `OneShotLocationDelegate.forceResolve()` 追加

**CardMode/CardKeychain.swift**
- A-16: `SecItemAdd` 失敗時に `#if DEBUG` ログ追加

**Localizable.xcstrings**
- Added: `business.entry.defaultName` (15言語)

---

## 2026-04-21 — Bug Hunt Round 10 (Multi-Agent Full Scan)

### NFCTool iOS — R10: i18n hardcodes, timeouts, safety, UX

**SettingsView.swift**
- B-05: 「管理ダッシュボード」→ `settings.dashboard.link` ローカライズキー化 (15言語)

**CardMode/CardModeRootView.swift**
- B-02: TextField placeholder "nextcode" → `card.create.userId.placeholder` キー化

**CardMode/CardManager.swift**
- A-08: `suggestUsername` — `20 - String(i).count` が負になる問題を `max(0, ...)` で修正
- A-07: `refreshCard` 失敗時に非ネットワークエラーを `lastError` に設定

**CardMode/SupabaseCardClient.swift**
- D-17: 全URLRequest に `timeoutInterval = 15` 追加（60秒デフォルト→15秒）

**PlaceSearchView.swift**
- D-04: クエリロード後に10秒タイムアウトをセット。JSが応答しない場合に `onFailed()` を呼び出しUIをアンブロック

**NFCTagLabelSync.swift**
- D-13: タイムアウト/デコードエラーで正確なメッセージを渡すよう修正

**TapHistoryView.swift**
- B-11: 「読み込み中...」「まだタップされていません」→ ローカライズキー化
- D-11: 日付パース失敗時にDEBUGログを出力（サイレントデータロスを可視化）

**TeamMode/TeamModeRootView.swift**
- B-13: メンバーグリッドセルを `onTapGesture` から `Button + UIImpactFeedbackGenerator` に変更

**TeamMode/TeamManager.swift**
- A-09: memberTokens Keychainデコード失敗時に不正データを削除＋DEBUGログ出力

**Localizable.xcstrings**
- Added: `settings.dashboard.link`, `card.create.userId.placeholder`, `common.loading`, `tapHistory.emptyState` (各15言語)

---

## 2026-04-21 — Bug Hunt Round 9 (Final Edge Case Pass)

### NFCTool iOS — R9: Timer race, re-entrancy, empty token guard

**NFCWriter.swift**
- R9-D-03: Cancel `errorClearTask` at start of success path in `verifyAndFinish()` — prevents stale 6s timer from erasing success banner after fail→succeed sequence

**CopyFlowView.swift**
- R9-D-04: `.sourceRead` retry button now has `.disabled(isBusy)` + `primaryButtonStyle(isEnabled: !isBusy)` — prevents double-tap while session is active

**HistoryManager.swift**
- R9-D-08: Added `isSaving` re-entrancy guard to `save()` — prevents duplicate write if iCloud notification fires during an in-progress save

**StoreManager.swift**
- R9-D-07: IAP sync now guards `!accessToken.isEmpty` before making network call — prevents sending `Bearer ` with empty token

---

## 2026-04-21 — Bug Hunt Round 8 (Final Sweep)

### NFCTool iOS — R8: Widget i18n fix

**NFCToolApp.swift**
- R8-02: Widget recent-tap time display replaced hardcoded Japanese strings with `RelativeDateTimeFormatter` — all device languages now render correctly

---

## 2026-04-21 — Bug Hunt Round 7

### NFCTool iOS — Multi-Agent R7 Audit: Thread Safety, UX Accuracy, Edge Cases

**TapHistoryView.swift**
- R7-A-01: HTTP error handling now distinguishes 401/403/5xx with specific localized messages
- R7-B-xx: `relativeTime` and `formattedTime` replaced hardcoded Japanese strings with `RelativeDateTimeFormatter` and system `DateFormatter` — all languages now display correctly

**WiFiPhotoPicker.swift**
- R7-B-03: `.onAppear` camera auto-open now guarded (`capturedImage == nil && !showImagePicker`) — prevents camera reopening every time image picker dismisses

**LocationManager.swift**
- R7-D-02: Added `isRequesting` flag to prevent re-entrant `requestOnce()` calls — all exit paths (success/fail/denied) reset the flag

**SNSGridEditorView.swift**
- R7-C-01: Pro gate moved from "Done" button to individual edit operations (move/delete/add/reset) — prevents free users from making changes that persist without a paywall gate

**SNSIDHistory.swift**
- R7-thread: Added `@MainActor` annotation; `cloudStoreDidChange` converted to `@objc nonisolated` with `Task { @MainActor }` to match Swift concurrency model

**SNSGridOrder.swift**
- R7-thread: Same `@MainActor` + `@objc nonisolated` pattern applied (same class, same issue)

**NFCShortener.swift**
- R7-D-04: Added URL length guard (`target.count <= 2048`) before POST
- R7-D-04: Added `decoded.id.isEmpty` guard with descriptive error when server returns empty short ID

**Localizable.xcstrings**
- Added `tapHistory.error.signInRequired`, `authExpired`, `forbidden`, `serverError`, `fetchFailed` keys

---

## 2026-04-21 — Bug Hunt Round 6

### NFCTool iOS — Multi-Agent R6 Audit: Session Safety, State Correctness, UX, Widget

**NFCAnalyzer.swift**
- R6-D-06: Added `guard !isReading else { return }` at top of `analyze()` to prevent double-session creation on rapid tap

**CopyFlowView.swift**
- R6-D-15: `.idle` case button now applies `.disabled(isBusy)` and `primaryButtonStyle(isEnabled: !isBusy)` — `isBusy` was defined but never wired to the button

**WidgetSharedData.swift**
- R6-D-11: `updateSNSLinks` now uses `guard` with fallback print instead of silent skip — failure is logged; `reloadWidgets()` is never called on invalid data (was: might call after silent nil)

**HistoryManager.swift**
- R6-C-07: Cloud trim loop now drops 20% (1/5) per iteration instead of 10% (1/10) — guarantees convergence even for items approaching 180 KB each
- R6-D-13: `mergeItems` now resolves UUID conflicts by timestamp (newer date wins) instead of always preferring local; return early if no cloud additions detected

**HistoryView.swift**
- R6-B-09 (deepen): Replaced `overlay(alignment:.bottom)` with `safeAreaInset(edge:.bottom)` for ResultBanner — banner now renders above tab bar via safe area rather than fixed padding

**PaywallView.swift**
- R6-B-05: Product loading area now distinguishes 3 states: `productsLoading` (ProgressView), `lastErrorMessage != nil` (error icon + message), and initial (ProgressView) — was always showing ProgressView

**Localizable.xcstrings**
- Added `paywall.loadingFailed` key in 15 languages

---

## 2026-04-21 — Bug Hunt Round 5

### NFCTool iOS — Security + Write Accuracy + UI Audit

**BusinessGroupDetailView.swift**
- R5-A-01: Team edit token migrated from UserDefaults (plaintext) to Keychain; legacy migration on first launch
- R5-C-02: Removed dead `creditWasDeducted` state variable

**SimpleWriteView.swift**
- R5-A-02: Added `lastWrittenURL` state; `onChange(of: message)` now records the actual written URL instead of `fullURL` (current UI state), fixing wrong URL in history on quick-rewrites from the history dropdown
- R5-D-01: Quick-rewrite button in history dropdown now checks `store.hasProAccess` before writing

**AnalysisView.swift (RecordFixSheet)**
- R5-A-05: Write button label shows ProgressView during `isShortening` (was only `isWriting`)

**HistoryView.swift**
- R5-B-01: `HistoryRow.isWriting` now includes `nfcWriter.isShortening` — prevents double-tap during URL shortening
- R5-C-03: Section titles use `String(localized:)` keys `history.section.writes` and `history.section.taps`

**FriendCardView.swift**
- R5-B-02: `navigationTitle` corrected to `"friendCard.navigationTitle"`

**AnalysisView.swift**
- R5-B-03: `navigationTitle` corrected to `"analysis.navigationTitle"` (key was already defined)

**ToolFormView.swift**
- R5-B-04: `.url` tool now calls `nfcWriter.write(url:)` instead of `writeURI()` so http(s) URLs go through the nfc.bz shortener (tap tracking + remote update)

**Localizable.xcstrings**
- Added `friendCard.navigationTitle` (ja/en)
- Added `history.section.writes` (ja/en)
- Added `history.section.taps` (ja/en)

---

## 2026-04-21 — Write Counting Full Audit (Round 4)

### NFCTool iOS — All Write Paths Verified

**SearchWriteView.swift**
- NEW-01: `onChange(of: isSuccess)` → `onChange(of: message)` for consecutive write reliability
- NEW-01: `shortenAs: .review` → `shortenAs: .url` (search results are generic URLs, not review tags)
- `historyManager.add()` + widget refresh now fires on every successful write

**FriendCardView.swift**
- NEW-02: Was completely missing `history.add`, `onChange`, disabled guard, and shortener path
- http(s) URLs now route through `write(url:)` (shortener); other schemes use `writeURI()`
- Added `onChange(of: nfcWriter.message)` recording history; added disabled guard

**CardWriteView.swift**
- NEW-03: Added `history.add(url:type:label:kind:"card")` via `onChange(of: writer.message)`
- Added write-in-progress UI (ProgressView spinner, gray button) + disabled guard

**TeamMemberDetailView.swift**
- NEW-04: Added `history.add(url:type:label:kind:"team")` via `onChange(of: writer.message)`
- Added write-in-progress UI + disabled guard

**AnalysisView.swift (RecordFixSheet)**
- NEW-05: `writer.writeURI(finalValue)` → conditional: `write(url:)` for http(s), `writeURI()` for other schemes
- NEW-05: `onChange(of: writer.isSuccess)` → `onChange(of: writer.message)`

---

## 2026-04-21 — Bug Hunt Round 3

### NFCTool iOS

**AuthManager.swift**
- A-11: `restoreSession` replaced with `withTaskGroup` pattern — auth work and 5-s timeout now race; winner cancels loser. No longer guarantees 5 s delay on every cold launch.

**HistoryManager.swift**
- D-12: iCloud KVS trim loop now properly re-encodes on each iteration; `?? cloudData` fallback removed to prevent infinite loop on encode failure; empty-array fallback added when cloudItems is fully drained.

**HistoryView.swift**
- A-10: `onChange` rewired from `nfcWriter.isSuccess` to `nfcWriter.message` so consecutive rewrites (isSuccess stays `true`) correctly append to history.
- B-10/C-07: `navigationTitle` corrected from `sns.navigationTitle` to `tab.history`.
- B-11: ResultBanner overlay gets `.padding(.bottom, 12)` to clear system tab bar.

**ToolFormView.swift**
- C-10: Two separate `onAppear` closures merged into one, eliminating load-order ambiguity (review history + tool history + saved values now all in a single handler).

**WriteEventUploader.swift**
- C-08: Duplicate blocked-scheme guard in `migrateICloudHistoryIfNeeded` reduced to single `isBlocked()` call.

**BusinessGroupDetailView.swift**
- B-13: "Company URL" tab in custom tab bar changed from `Button { onBack() }` to a non-interactive active-state indicator (filled icon + semibold label).

---

## 2026-04-21 — Bug Hunt Round 1

### NFCTool iOS

**NFCWriter.swift**
- A-01: `lockTagOnly()` now resets `isSuccess = false` before starting session
- D-06: Added `cancelSession()` public method; called from ToolFormView on `.background` scenePhase

**ToolFormView.swift**
- D-06: Added `@Environment(\.scenePhase)` + `.onChange(of: scenePhase)` to cancel NFC session on background
- StoreManager.swift: Removed redundant `trialStartKey`; migration now removes UserDefaults entry after writing to Keychain (C-05)

### ReviewTap iOS

**ToolFormView.swift**
- C-01: Write button now checks `nfcWriter.isShortening` in both disabled/label guard (parity with NFCTool)
- D-06: Added scenePhase background handler to cancel NFC session

**TagDetailView.swift**
- D-04: `save()` validates redirect URL (non-empty, http/https, non-empty host) before network call

**NFCWriter.swift**
- A-01: `lockTagOnly()` now resets `isSuccess = false`
- D-06: Added `cancelSession()` public method

**StoreManager.swift**
- C-05: Removed redundant `trialStartKey`; migration removes UserDefaults entry after Keychain write

**Localizable.xcstrings**
- Added `reviewtap.tag.error.invalidRedirectUrl` (en + ja)

### Prior Sessions (summary)

| Date | Change |
|------|--------|
| 2026-04-20 | nfc.bz: await logTap fix; in() filter encodeURIComponent fix |
| 2026-04-19 | iOS: migrate to nfc.bz API + subdomain URLs |
| 2026-04-18 | Widget: redesign UI, tap analytics, dashboard link |
| 2026-04-17 | Security: forceUnlockAll SCREENSHOT_MODE, consumable dedup, Keychain trial, 1MB iCloud trim |
