Skip to content

refactor: clear some STRICTNESS_MIGRATION suppressions - #13873

Open
gkartalis wants to merge 2 commits into
mainfrom
gkartalis/strictness-migration-pt1
Open

refactor: clear some STRICTNESS_MIGRATION suppressions#13873
gkartalis wants to merge 2 commits into
mainfrom
gkartalis/strictness-migration-pt1

Conversation

@gkartalis

@gkartalis gkartalis commented Aug 3, 2026

Copy link
Copy Markdown
Member

This PR resolves []

Description

Removes most of the remaining @ts-expect-error STRICTNESS_MIGRATION / as any suppressions. Marker count goes from 143 across 43 files on main to 63 across 22 files.

Most of the diff is type-only (proper param/return annotations, NonNullable<> instead of asserting through nullable Relay fields) with no runtime change. The exceptions are called out below.

Deliberate behavior changes

  • BidButton: returns null instead of throwing when sale is missing (replaces a TODO: Do we need a nil check against +sale+?), and no longer calls redirectToBid when the bid increment is unavailable.
  • SendConversationMessage: skips the optimistic update instead of throwing when the conversation record, message edge, or connection can't be found.
  • Gemini upload helpers: createGeminiAssetWithS3Credentials / getGeminiCredentialsForEnvironment reject with a real Error on a null asset/credentials payload instead of throwing on a null property access.
  • FeaturedArtists: tracking is a no-op when the tracking prop is undefined, rather than throwing.
  • FullFeaturedArtistList: fixes an invalid spacing value (pb={20}pb="20px").
  • useScreenDimensions: ScreenDimensionsContext is seeded with real dimensions instead of null as any, so consumers rendered outside ProvideScreenDimensions get usable values instead of crashing.
  • uploadFileToS3: reads request.status directly rather than off the (untyped) event target.
  • DeepZoomTile: types the static _cache and adds two scoped react-hooks/rules-of-hooks disables for hook calls gated by the module-level VISUAL_DEBUG_MODE constant — surfaced while touching this file, no runtime change.

Dead code removal

  • Deletes AuctionPrice.tsx and its test file (~400 lines). Nothing imports it — it was left behind by an earlier refactor, and it only came up because it carried suppressions.

Intentionally left suppressed

Review flagged five spots where clearing the suppression meant picking new null-handling behavior. Rather than make those calls in a cleanup PR, they keep their existing suppressions and are deferred to a follow-up:

  • MakeOfferButton.tsx / InquiryMakeOfferButton.tsx — a null mutation payload needs a real error path, not a silent return.
  • FeaturedArtists.tsx — the artist counts used for the header and "View all" link need to agree with what actually renders.
  • DeepZoomOverlay.tsx — needs the call-site guard in ImageZoomView tightened before the nested deepZoom.image access can be made safe.
  • uploadFileToS3.tsformData loop, left as-is.

The other remaining markers (55 of 63) are in the legacy City Guide / Map scenes and their helpers, which are out of scope here.

PR Checklist

  • I have tested my changes on the following platforms:
    • Android.
    • iOS.
  • I hid my changes behind a [feature flag], or they don't need one.
  • I have included screenshots or videos at least on Android, or I have not changed the UI.

### Description

Removes the remaining `@ts-expect-error STRICTNESS_MIGRATION` / `as any`
suppressions left after the first pass (which covered the zero-behavior-change
fixes). This batch fixes real null-safety holes that the suppressions were
hiding, so each one is a small, deliberate behavior change:

- BidButton: renders nothing instead of throwing when `sale` is missing.
- MakeOfferButton / InquiryMakeOfferButton: guard a null mutation payload
  instead of throwing inside the Relay callback.
- SendConversationMessage: skip the optimistic update instead of throwing
  when the connection/edge isn't found.
- Gemini upload helpers: reject with a real error instead of throwing on a
  null asset/credentials payload.
- FeaturedArtists: filter out null artist entries instead of crashing on
  `.internalID`; no-op tracking if `tracking` is undefined.
- FullFeaturedArtistList: fix an invalid spacing value (`pb={20}` → `pb="20px"`).
- useScreenDimensions: seed the context with real dimensions instead of `null`.
- DeepZoomTile: silence two known-safe conditional hook calls gated by a
  module-level debug constant, surfaced while touching this file.

`yarn tsc` is clean repo-wide. Remaining `STRICTNESS_MIGRATION` markers are
confined to the legacy City Guide / Map scenes, out of scope for this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gkartalis gkartalis self-assigned this Aug 3, 2026
@gkartalis gkartalis added the FF label Aug 3, 2026
Comment thread src/app/Scenes/Artwork/Components/CommercialButtons/MakeOfferButton.tsx Outdated
Comment thread src/app/Scenes/Inbox/Components/Conversations/InquiryMakeOfferButton.tsx Outdated
Comment thread src/app/Scenes/Collection/Components/FeaturedArtists.tsx
Comment thread src/app/Components/PhotoRow/utils/uploadFileToS3.ts Outdated
Comment thread src/app/Scenes/Artwork/Components/CommercialButtons/MakeOfferButton.tsx Outdated
Comment thread src/app/Scenes/Collection/Components/FeaturedArtists.tsx Outdated
Comment thread src/app/Scenes/Artwork/Components/CommercialButtons/BidButton.tsx
Restores the STRICTNESS_MIGRATION suppressions in the five spots review
flagged, keeping the unrelated cleanups in those files. To be revisited
separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@artsyit

artsyit commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

This PR contains the following changes:

  • Dev changes (refactor - clear some STRICTNESS_MIGRATION suppressions - gkartalis)

Generated by 🚫 dangerJS against 94c9839

@github-actions github-actions Bot deleted a comment from claude Bot Aug 3, 2026
@github-actions github-actions Bot deleted a comment from claude Bot Aug 3, 2026
block
// @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏
onPress={() => redirectToBid(incrementCents)}
onPress={() => incrementCents != null && redirectToBid(incrementCents)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The != null guard turns the Bid button into a silent no-op when the increment has no cents.

BidIncrementsFormatted.cents is nullable in the schema (data/schema.graphql:7234cents: Float), so incrementCents can be null at runtime. Before, redirectToBid(null) at least pushed the user to the bid screen (with a broken ?bid=null); now the primary commerce CTA does nothing at all and gives no feedback.

There's also an inconsistency: when saleArtwork.increments is empty, firstIncrement short-circuits to 0, so incrementCents is 0, which passes != null and navigates with ?bid=0. Two adjacent "no usable increment" cases behave differently.

Either handle the missing value explicitly (navigate without a bid param, or disable the button) or keep the previous navigation and fix the URL — but the current shape makes a real failure invisible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one might be worth paying attention to, would be curious what happens now when incrementCents is null or undefined, if you really can't bid in this case it probably makes sense to disable the button and possibly throw something?

Comment on lines +33 to +42
if (!conversationStore || !newMessageEdge) {
return
}
const connection = ConnectionHandler.getConnection(
// @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏
conversationStore,
"Messages_messagesConnection"
)
// @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏
if (!connection) {
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 These bare returns swallow a failure the user will notice.

storeUpdater runs as both optimisticUpdater and updater. If the connection or edge isn't found in the updater path, the mutation still succeeded on the server — the message was sent — but it never gets inserted into Messages_messagesConnection, so the thread looks like the message vanished. Before, the throw at least surfaced in logs.

Add observability so this doesn't become a silent class of bug:

if (!conversationStore || !newMessageEdge) {
  captureMessage("sendConversationMessage: missing conversation record or message edge")
  return
}

(same for the !connection case). captureMessage from @sentry/react-native is already used elsewhere in the app, e.g. src/app/Scenes/Artwork/Components/ImageCarousel/ImageCarousel.tsx:195.

)
}

// eslint-disable-next-line react-hooks/rules-of-hooks -- VISUAL_DEBUG_MODE is a module-level constant, so this condition never changes across renders of a given instance

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 This suppression is avoidable. useSpringValue doesn't depend on anything inside the VISUAL_DEBUG_MODE block — hoisting it up next to the other hooks (above line 89) makes the call unconditional and removes the need for the disable. The debug branch pays nothing since VISUAL_DEBUG_MODE is false (__deepZoomDebug.ts:1).

The useEffect one at line 91 genuinely needs the escape hatch, so that can stay.

@claude

claude Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🅰️ Single-pass review (baseline)

Summary

Second pass at clearing @ts-expect-error STRICTNESS_MIGRATION / as any suppressions, plus deletion of the dead AuctionPrice component and its test. Most of the diff is genuinely zero-behavior type work — Timer/StateManager/DurationProvider nullability, the two Relay middlewares, Grid/Title prop types, DeepZoomTileID._cache, the zoomViewRefs array — and that part reads well. A handful of spots are real behavior changes, and those are where the comments below land.

I verified the AuctionPrice deletion is clean: no remaining references anywhere in src/, and app/__fixtures__/ArtworkBidInfo is still used by ArtworkLotDetails.tests.tsx, so it correctly stays.

Issues Found

🟡 Important

  1. BidButton.tsx:262 — the new != null guard makes the Bid button a silent no-op. BidIncrementsFormatted.cents is nullable (data/schema.graphql:7234), so this is reachable. Details inline.

  2. SendConversationMessage.ts:33-42 — bare returns hide a user-visible failure. The message is sent but never enters the connection, so the thread looks like it vanished. Details inline.

  3. The PR description no longer matches the diff. After 94c9839 (the revert), these bullets are stale:

    • "FeaturedArtists: filter out null artist entries" — FeaturedArtists.tsx still has suppressions at lines 24, 27, 43 and 48; only the tracking?. change survived.
    • "MakeOfferButton / InquiryMakeOfferButton: guard a null mutation payload" — both still carry the suppression (MakeOfferButton.tsx:108, InquiryMakeOfferButton.tsx:97).
    • "Remaining STRICTNESS_MIGRATION markers are confined to the legacy City Guide / Map scenes" — not true. Also still present in Components/Icons/PinFairSelected.tsx, Components/Icons/PinSavedOff.tsx, PhotoRow/utils/uploadFileToS3.ts:73 (a file this PR edits), DeepZoom/DeepZoomOverlay.tsx, plus the three above.

    Worth updating so the next pass knows what is actually left.

🟢 Suggestions

  1. DeepZoomTile.tsx:120 — the useSpringValue suppression is avoidable by hoisting the call. Inline.

  2. BidButton.tsx:78-80 (pre-existing, adjacent to your watchOnly retype) — the tracked action name is inverted against the label: line 181 renders isWatchOnly ? "Watch live bidding" : "Enter live bidding", while the tracking payload is isWatchOnly ? EnterLiveBidding : WatchLiveBidding. Every live-bidding tap reports the opposite action. Out of scope here, but someone should pick it up.

  3. useScreenDimensions.tsx:34 — swapping null as any for a real default removes the type lie, but it also removes the loud failure. A consumer rendered outside ProvideScreenDimensions used to crash immediately; now it silently gets safeAreaInsets: {0,0,0,0} and lays out under the notch. The default is also a snapshot taken at module load, so it never reflects rotation. Both are acceptable for a context default — just flagging that the failure mode moved from loud to quiet.

  4. ImageCarouselContext.tsx:154React.createContext<ImageCarouselContext>(null as any) is the same pattern you just fixed in useScreenDimensions, in a file this PR already touches. Fine to leave, but it is the obvious next one.

  5. Test typing is a lateral move. as ImageCarouselImage[] (three carousel test files) and as unknown as ComponentProps<typeof ...> (CollectionArtistSeriesRail.tests.tsx, FeaturedCollectionsRail.tests.tsx) are as unsound as the any they replace — they just relocate the assertion. Not blocking; typing the fixtures properly would be the real fix.

  6. No tests for the new guard paths. BidButton with a null sale, the two sendConversationMessage early returns, and the two Gemini reject branches are all new branches with no coverage. BidButton.tests.tsx exists and would be a cheap place to add the null-sale case.

Areas Reviewed

  • Architecture & Design — no cross-scene imports introduced; the four // eslint-disable-next-line no-restricted-imports additions for navigate are the escape hatch the rule's own message prescribes (.eslintrc.js:149), so they are correct, not a workaround.
  • Bugs & Edge Cases — checked the ones that looked risky and they are fine: uploadFileToS3's e.target.status to request.status is equivalent for RN's XHR; compact(saleArtwork?.increments ?? []) strips null elements, not falsy fields; media.map((m) => ({ width: m.width ?? 0, ... })) produces the same NaN-then-|| 0 path getMeasurements already had, so no visual change; pb={20} to pb="20px" resolves to the same 20px since neither key exists in the space scale (and matches pb="15px" at FeaturedArtists.tsx:69); AssetCredentials as NonNullable<...> is safe because policyDocument: S3PolicyDocumentType! is non-null in the schema; TickerState.date going nullable is handled by DurationProvider (guards at lines 19, 33, 60); GridProps' odd-looking flex?: ... | null is needed by the real flex={null} at BidInfoRow.tsx:26.
  • Performance — nothing new. The media.map in ImageCarouselEmbedded runs on every render, but getMeasurements already did.
  • Security — nothing relevant.

Questions for Author

  • The commits list Claude Sonnet 5 and Claude Opus 5 as co-authors, but the description does not disclose it. Per RFC: disclose LLM usage in commits or PRs, please add an Assisted by: ... line.
  • Screenshots/recordings on both Android and iOS, please. FullFeaturedArtistList's pb change and BidButton's new early return are both user-visible, and the checklist boxes are unticked.
  • Item 3 above: is leaving MakeOfferButton / InquiryMakeOfferButton / FeaturedArtists suppressed the intended end state for this PR, or is a follow-up already planned? A tracking issue would help.

@claude

claude Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🅱️ Two-pass review (candidate)

Summary

Clears ~80 @ts-expect-error STRICTNESS_MIGRATION / as any suppressions across 33 files, mostly by adding real param/return annotations and NonNullable<> unwrapping of nullable Relay fields. Deletes the unreferenced AuctionPrice component and its test. The deliberate behavior changes (null guards in BidButton, SendConversationMessage, the Gemini upload helpers) are all called out in the description, and the five spots that needed a real product decision were reverted back to suppressions rather than guessed at — good call.

Checked the deletion: nothing in src/ or data/ references AuctionPrice or AuctionPrice_artwork, so it's safe. The pb={20}pb="20px" change is visually identical (styled-system falls through to the raw value either way) and matches 28 existing p*="Npx" call sites, including FeaturedArtists.tsx:69 in the same folder.

Issues Found

🟡 Important

BidButton.tsx:249-262 — the new incrementCents != null guard can never be false, and the case it is meant to catch still navigates with bid=0.

const increments = compact(saleArtwork?.increments ?? []).filter(
  (increment) => (increment.cents ?? 0) > (myLastMaxBid || 0)
)
const firstIncrement = increments && increments.length && increments[0]
const incrementCents = firstIncrement && firstIncrement.cents
...
onPress={() => incrementCents != null && redirectToBid(incrementCents)}

The filter already drops any increment with a null cents: (null ?? 0) > (myLastMaxBid || 0) is 0 > n where n >= 0, always false. So whenever firstIncrement is an object, its cents is non-null, and incrementCents != null is always true.

The one case where the increment really is unavailable — increments empty, either because saleArtwork is null or because the user's own max bid already exceeds every increment — takes the && chain to 0, not null: increments.length is 0, so firstIncrement === 0 and incrementCents === 0. 0 != null passes, and redirectToBid(0) navigates to /auction/<sale>/bid/<artwork>?bid=0.

So the described "no longer calls redirectToBid when the bid increment is unavailable" does not happen. The net effect of this line is nil, and the ?bid=0 path is still there.

The x && x.length && x[0] idiom is what produces the 0. Suggest:

const firstIncrement = increments[0]
...
onPress={() => firstIncrement && redirectToBid(firstIncrement.cents!)}

and then either disable the button or navigate without the bid param when there is no increment — a silently dead but enabled "Bid" CTA also swallows the tappedBid / IncreaseMaxBid event, since trackEvent lives inside redirectToBid.

The same idiom in getMyLotStanding (line 30) gives it a 0 | null | undefined | LotStanding return type, which is why getHasBid has to take ReturnType<typeof getMyLotStanding>. artwork.myLotStanding?.[0] cleans up both.

🟢 Suggestions

Registration.tests.tsx (~14 sites) — onCompleted?.(...) weakens the assertions. If the component ever stops passing onCompleted/onError, the mock silently does nothing and every assertion below it goes vacuous instead of failing. @typescript-eslint/no-non-null-assertion is OFF for *.tests.tsx (.eslintrc.js:163-166), so onCompleted!(...) clears the suppression and keeps the loud failure.

No coverage for the new guard branches. BidButton.tests.tsx asserts the label renders but never presses the button, so neither the sale: null early return nor the increment path is exercised. The Gemini helpers have no test file at all (Components/PhotoRow/utils/gemini/ holds only sources), so the two new reject(new Error(...)) paths are untested. Also, the !token check in createGeminiAssetWithS3Credentials.ts:34 rejects on "" too, where token == null would be the exact-equivalent check.

FeaturedArtists.tsx:4 — stray blank line inserted mid-import-block, between ArtistListItem and the navigate import. import/order has no newlines-between setting, so lint won't catch it.

src/app/__fixtures__/ArtworkBidInfo.tsClosedAuctionArtwork and LiveAuctionInProgeress are now dead exports. Deleting AuctionPrice.tests.tsx removed their only consumer; ArtworkLotDetails.tests.tsx imports the other ten.

DeepZoomTile.tsx:91,120 — the two rules-of-hooks disables are avoidable. The justification comment is accurate, but hoisting useEffect and useSpringValue above the if (VISUAL_DEBUG_MODE) block removes the need for either directive. Worth checking whether the line-120 one fires at all — the repo doesn't run --report-unused-disable-directives, so a dead directive wouldn't error.

Areas Reviewed

  • Bugs & edge cases: the SendConversationMessage guards are fine — the key at line 38 (Messages_messagesConnection) does match the @connection key at Messages.tsx:221, so the null branch is defensive rather than load-bearing. Leaving mutationPayload unguarded matches the repo's other getRootField call site (useCreateNewArtworkList.ts:66). The useScreenDimensions context default is effectively unreachable — Providers.tsx wraps the tree and setupJest.tsx:610 mocks the provider. uploadFileToS3's e.target.statusrequest.status is the same value.
  • Types: media: ImageCarouselImage[] & ImageCarouselVideo[]ImageCarouselMedia[] is a real fix — the old intersection made item.__typename uninhabitable, and the __typename === "Video" narrowing in ImageCarouselFullScreen.tsx:120 now works properly. bidderNeedsIdentityVerification returning !!(...) instead of a boolean | null | "" union tightens all four call sites.
  • Performance: media.map(...) in ImageCarouselEmbedded allocates one array per render, but getMeasurements was already called inline each render, so this is noise.
  • Architecture: the four // eslint-disable-next-line no-restricted-imports comments for navigate are exactly the escape hatch the rule message documents (.eslintrc.js:145-150), and are needed for lint to pass on these now-touched files.

Questions for Author

  • The PR checklist has both iOS and Android testing unchecked. BidButton (auction CTA) and SendConversationMessage (message send) are the risky paths — worth a manual pass on both platforms before merge.
  • Both commits carry Co-Authored-By: Claude trailers but the description doesn't disclose LLM assistance. Per artsy/README#582, add an Assisted by: Opus/Sonnet line to the description.

jest.fn<typeof relay.commitMutation, Parameters<typeof relay.commitMutation>>(fn as any)
const commitMutationMock = (
fn?: (environment: any, config: MutationConfig<MutationParameters>) => Disposable | null
) => jest.fn(fn)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brainbicycle brainbicycle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great, the silent fail on bidIncrements I think worth a second look, if you already looked feel free to disregard

@MounirDhahri MounirDhahri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice nice nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants