Initials, shared. Home's partner bubble and the Settings "Connected with" row
are two views of one person and gave different answers when there was no photo —
Home showed initials, Settings showed a heart. Settings now shows the partner's
initials in a 48dp circle, so the photo and no-photo states share one
silhouette. The helper is lifted to ui/components/Initials.kt rather than copied;
an identical copy in two files is exactly how packArtworkRes drifted. The
unpaired state keeps the heart — there is nobody to take initials from yet.
Framing. Picking a photo now opens a crop sheet: pinch to zoom, drag to move,
inside the real avatar circle. The avatar was a blind ContentScale.Crop, so a
non-square or off-centre photo was centre-cropped and could lose the subject —
Ava's 640x480 test photo cropped straight past her face. The crop is applied at
pick time and handed back as a normal Uri, so setPhotoUri → upload is untouched.
EXIF orientation is honoured (a portrait selfie would otherwise crop sideways),
output is a 512px JPEG, and the source aspect is respected.
Two bugs found by driving it live, both of which looked fine in code:
- Panning did nothing at 1x. The clamp was viewport*(scale-1)/2, which is 0 at
1x — correct only for a square source. A cover-fit 640x480 photo already
overflows horizontally at 1x, so that pinned it dead centre and made framing
impossible: the whole point of the sheet. Now clamped to the picture's actual
overflow, computed from the source aspect.
- Panning then revealed empty space at the circle's edge. ContentScale.Crop had
already discarded the overflow and returned a viewport-sized node, so
graphicsLayer was sliding an already-cropped square around — the pixels being
panned to had been thrown away first. The preview now draws the bitmap itself
at cover*scale with the same maths as the crop, so preview == output.
Adds androidx.exifinterface. Verified live on 5554: sheet opens, pan moves the
picture (pixel-probed), no empty edge. 0 FATAL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Critical fix: Purchases was never told the Firebase uid, so RevenueCat assigned its
own anonymous app_user_id. The revenueCatWebhook Cloud Function writes premium status
to users/{app_user_id}/entitlements/premium using that id — meaning a real purchase
would silently never unlock premium for the signed-in account, and CouplePremiumChecker's
partner-side read (same path, different uid) was broken by the same root cause.
RevenueCatBillingRepository now collects AuthRepository.authState and calls
Purchases.awaitLogIn(uid) on sign-in / awaitLogOut() on sign-out, guarded against
redundant calls and wrapped best-effort so a failed sync retries on the next auth event
instead of crashing the singleton.
Also:
- Bump com.revenuecat.purchases 8.20.0 -> 10.12.0 (verified: real published version,
stable API surface across 8->10 per RevenueCat's own migration notes for the calls
this app uses; confirmed resolved + full Hilt/KSP graph compiles clean).
- Purchase cancellation (user backs out of the Play billing sheet) is now distinguished
from a real failure via PurchasesTransactionException.userCancelled, using a shared
PURCHASE_CANCELLED_SENTINEL (same marker-constant idiom PaywallViewModel already uses
for offering-load failures) so PaywallViewModel resets silently instead of surfacing
the SDK's internal error text.
- PaywallScreen: genuine purchase errors (billing unavailable, network, etc.) now show
a snackbar. Previously there was zero user-facing feedback on a real purchase failure
beyond the loading spinner disappearing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Centralize all plugin + dependency versions in gradle/libs.versions.toml (the
modern Gradle standard) and switch the root + app build scripts to the generated
libs.* / alias(libs.plugins.*) accessors. No version changes — a 1:1 migration of
the 57 hardcoded coordinates. Full assembleDebug + unit suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>