Skip to content

feat(user-profile): enforce upload policy on photo selection - #1122

Merged
bmc08gt merged 2 commits into
code/cashfrom
feat/flipcash-upload-policy-enforcement
Jul 23, 2026
Merged

feat(user-profile): enforce upload policy on photo selection#1122
bmc08gt merged 2 commits into
code/cashfrom
feat/flipcash-upload-policy-enforcement

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What

Enforces the server-provided UploadPolicy on profile photo selection in PhotoSelectionViewModel. The policy was already loaded into state but never actually consulted — picks were cached, re-encoded, and uploaded regardless of what the server accepts.

Changes

  • MIME acceptance — reject picks whose type the policy doesn't accept. Checked against the re-encoded upload MIME (uploadMimeFor(...)), not the raw source, so formats that normalize into an accepted type (e.g. HEIC/WebP → PNG) still pass.
  • Dimensions + pixels — the downscale target now comes from the policy instead of a hardcoded 500. Since copyToCache bounds the longest edge, maxEdgeFor() takes min(maxWidth, maxHeight, √maxPixels), which satisfies both dimension caps and the total-area cap. Falls back to 500 when the policy names no image constraints.
  • Byte ceiling — the re-encoded output is measured against maxSizeBytes before upload; oversized picks are rejected. Adds ContentReader.size(uri) for the measurement.
  • Rejections clear the pending preview and surface an alert (new imageTooLarge strings reuse the existing alert pattern).

Behavior notes

  • Fails open when the policy hasn't loaded — consistent with it being advisory; initiateExternalUpload + moderation remain authoritative server-side.
  • Does not retry compression at reduced quality when over maxSizeBytes — it rejects instead. For downscaled profile images that ceiling is very unlikely to be hit, so a transcode-retry loop felt like over-engineering. Easy follow-up if desired.

Testing

  • :ui:resources and :apps:flipcash:features:user-profile compile clean.
  • No existing unit tests cover PhotoSelectionViewModel / ContentReader (and no test fakes implement the interface), so this is compile-verified.

@github-actions github-actions Bot added type: feature New functionality area: ui Compose UI, theme, components, resources labels Jul 22, 2026
@bmc08gt
bmc08gt force-pushed the feat/flipcash-upload-policy-enforcement branch from e54e14c to baa42fa Compare July 23, 2026 01:39
bmc08gt added 2 commits July 23, 2026 11:09
Gate profile photo picks against the server UploadPolicy before caching
and upload:

- Reject MIME types the policy doesn't accept (checked against the
  re-encoded upload type, not the source, so HEIC/WebP that normalize
  into an accepted format still pass).
- Derive the downscale target from the policy's dimension + pixel caps
  (min of maxWidth, maxHeight, and sqrt(maxPixels)) instead of a
  hardcoded 500px; falls back to 500 when no constraints are named.
- Enforce maxSizeBytes on the re-encoded output before upload, adding
  ContentReader.size() for the measurement.

Fails open when the policy hasn't loaded; the server remains
authoritative. Rejections clear the preview and surface an alert.
… errors

Migrate BlobState to a sealed Ready/Rejected model and update all
consumers, and surface per-category moderation feedback in the name and
photo flows.

BlobState (sealed):
- toBlobState() now yields BlobState? — READY -> Ready(metadata),
  REJECTED -> Rejected(reason), and non-terminal PENDING/PROCESSING/
  UNKNOWN -> null (callers keep polling). A REJECTED blob missing its
  reason falls back to UNKNOWN rather than being dropped.
- getBlobs uses mapNotNull so a still-processing id resolves to an empty
  list; awaitReady switches on the sealed type.
- BlobRejectedException takes a non-null BlobRejection.

Name/photo error handling:
- Photo: map BlobRejectedException by RejectionReason, and MODERATION by
  FlaggedCategory, to specific copy; other terminal reasons show a
  generic failure.
- Name: rely on SetDisplayNameError.FailedModerated and branch on
  FlaggedCategory for specific copy.
- Add per-category name/photo strings; fix subtitle typo.

Photo selection also resizes to fit the policy: shrink the longest edge
until the re-encoded bytes fit maxSizeBytes rather than rejecting, with
a last-resort reject only if the smallest re-encode still overflows.
@bmc08gt
bmc08gt force-pushed the feat/flipcash-upload-policy-enforcement branch from baa42fa to 615802c Compare July 23, 2026 18:37
@github-actions github-actions Bot added the area: network gRPC, connectivity, API, exchange rates label Jul 23, 2026
@bmc08gt
bmc08gt merged commit f8946ef into code/cash Jul 23, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the feat/flipcash-upload-policy-enforcement branch July 23, 2026 18:40
bmc08gt added a commit that referenced this pull request Jul 24, 2026
* feat(user-profile): enforce upload policy on photo selection

Gate profile photo picks against the server UploadPolicy before caching
and upload:

- Reject MIME types the policy doesn't accept (checked against the
  re-encoded upload type, not the source, so HEIC/WebP that normalize
  into an accepted format still pass).
- Derive the downscale target from the policy's dimension + pixel caps
  (min of maxWidth, maxHeight, and sqrt(maxPixels)) instead of a
  hardcoded 500px; falls back to 500 when no constraints are named.
- Enforce maxSizeBytes on the re-encoded output before upload, adding
  ContentReader.size() for the measurement.

Fails open when the policy hasn't loaded; the server remains
authoritative. Rejections clear the preview and surface an alert.

* feat(user-profile): sealed BlobState + granular name/photo moderation errors

Migrate BlobState to a sealed Ready/Rejected model and update all
consumers, and surface per-category moderation feedback in the name and
photo flows.

BlobState (sealed):
- toBlobState() now yields BlobState? — READY -> Ready(metadata),
  REJECTED -> Rejected(reason), and non-terminal PENDING/PROCESSING/
  UNKNOWN -> null (callers keep polling). A REJECTED blob missing its
  reason falls back to UNKNOWN rather than being dropped.
- getBlobs uses mapNotNull so a still-processing id resolves to an empty
  list; awaitReady switches on the sealed type.
- BlobRejectedException takes a non-null BlobRejection.

Name/photo error handling:
- Photo: map BlobRejectedException by RejectionReason, and MODERATION by
  FlaggedCategory, to specific copy; other terminal reasons show a
  generic failure.
- Name: rely on SetDisplayNameError.FailedModerated and branch on
  FlaggedCategory for specific copy.
- Add per-category name/photo strings; fix subtitle typo.

Photo selection also resizes to fit the policy: shrink the longest edge
until the re-encoded bytes fit maxSizeBytes rather than rejecting, with
a last-resort reject only if the smallest re-encode still overflows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: network gRPC, connectivity, API, exchange rates area: ui Compose UI, theme, components, resources type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant