From dc223304184ad398bd0836104769db8d5941f2f5 Mon Sep 17 00:00:00 2001 From: Cestercian Date: Wed, 29 Jul 2026 12:46:28 +0000 Subject: [PATCH] chore(parity): HeaderBar, CR test nits, docs after #45 merge. Port HeaderBar; cover MessagesFilter + checkout entitlement gate; sync CLAUDE/PARITY/FEATURE checklists now that parity-polish is on main. Co-Authored-By: Cursor Grok 4.5 Co-authored-by: Cestercian --- CLAUDE.md | 12 ++-- .../component/ComponentGallery.kt | 1 + .../app/designsystem/component/HeaderBar.kt | 67 +++++++++++++++++++ .../app/feature/paywall/EntitlementStore.kt | 4 +- .../booking/CheckoutViewModelLogicTest.kt | 27 ++++++++ .../messages/MessagesFilterLogicTest.kt | 31 +++++++++ docs/FEATURE_CHECKLIST.md | 21 +++--- docs/PARITY_CHECKLIST.md | 10 +-- 8 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/in/artistant/app/designsystem/component/HeaderBar.kt create mode 100644 app/src/test/java/in/artistant/app/feature/messages/MessagesFilterLogicTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index 8f00608..d288c95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,18 +142,14 @@ Milestones M0–M8 (see `docs/IMPLEMENTATION_ROADMAP.md`). Each is an issue. Update this section's "current state" line as phases land. -**Current state:** M0–M7 on `main` (PR #44). Follow-up **`feature/parity-polish`** -closes remaining partials: BookingDetail Message/Getting there/calendar/review; -Artist Home earnings + busy strip + quote requests; Messages All/Bookings/ -Inquiries filters; MonthDayGrid on Bookings/Gigs; Help/Feedback → `app_feedback`; -SearchRecents; checkout entitlement gate; dead DeepLinkRouter removed (TabRouter -is the live push path). Product truth: redaction retired, request→accept, -Airbnb chat trust. **Unit tests green**. +**Current state:** M0–M7 + parity polish on `main` (PR #44 + **PR #45**). Product +truth: redaction retired, request→accept, Airbnb chat trust. BookingDetail CTAs, +Artist Home dashboard, Messages filters, MonthDayGrid, Help/Feedback, SearchRecents, +community pledge, Avatar, Profile stats → ArtistList shipped. **Unit tests green**. **Still operator / follow-ups:** ExoPlayer sample/Spotify embeds, brand `.ttf`, `google-services.json` + `send-push` FCM, OAuth dashboard config, flip `subscriptionsEnabled`, PROF-* artist polish, M8 instrumented UI / Play upload. -(Profile stats + community pledge shipped on `feature/parity-polish`.) --- diff --git a/app/src/main/java/in/artistant/app/designsystem/component/ComponentGallery.kt b/app/src/main/java/in/artistant/app/designsystem/component/ComponentGallery.kt index f99e208..0d4926e 100644 --- a/app/src/main/java/in/artistant/app/designsystem/component/ComponentGallery.kt +++ b/app/src/main/java/in/artistant/app/designsystem/component/ComponentGallery.kt @@ -29,6 +29,7 @@ fun ComponentGallery() { verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text("Component gallery", style = AppTheme.type.displaySub, color = AppTheme.colors.ink) + HeaderBar(title = "HeaderBar", subtitle = "optional subtitle") PrimaryButton(text = "Filled", onClick = {}, fullWidth = true) PrimaryButton(text = "Ghost", onClick = {}, variant = ButtonVariant.Ghost, fullWidth = true) PrimaryButton(text = "Subtle", onClick = {}, variant = ButtonVariant.Subtle, fullWidth = true) diff --git a/app/src/main/java/in/artistant/app/designsystem/component/HeaderBar.kt b/app/src/main/java/in/artistant/app/designsystem/component/HeaderBar.kt new file mode 100644 index 0000000..2cc07ea --- /dev/null +++ b/app/src/main/java/in/artistant/app/designsystem/component/HeaderBar.kt @@ -0,0 +1,67 @@ +package `in`.artistant.app.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import `in`.artistant.app.designsystem.theme.AppTheme + +/** + * Compact screen header — port of iOS `HeaderBar`. + * Title (+ optional subtitle) between optional leading/trailing slots, + * with a hairline rule under the bar. + */ +@Composable +fun HeaderBar( + title: String, + modifier: Modifier = Modifier, + subtitle: String? = null, + leading: @Composable (() -> Unit)? = null, + trailing: @Composable (() -> Unit)? = null, +) { + val colors = AppTheme.colors + val space = AppTheme.dimens.space + Column(modifier.fillMaxWidth().background(colors.bg)) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = space.lg, vertical = space.md), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space.md), + ) { + Box(Modifier.widthIn(min = AppTheme.dimens.size.avatarSm), contentAlignment = Alignment.CenterStart) { + leading?.invoke() + } + Column(Modifier.weight(1f)) { + Text( + title, + style = AppTheme.type.headline.copy(fontWeight = FontWeight.Bold), + color = colors.ink, + ) + subtitle?.let { + Text(it, style = AppTheme.type.footnote, color = colors.ink3) + } + } + Box(Modifier.widthIn(min = AppTheme.dimens.size.avatarSm), contentAlignment = Alignment.CenterEnd) { + trailing?.invoke() + } + } + Spacer( + Modifier + .fillMaxWidth() + .height(AppTheme.dimens.size.hairline) + .background(colors.lineSoft), + ) + } +} diff --git a/app/src/main/java/in/artistant/app/feature/paywall/EntitlementStore.kt b/app/src/main/java/in/artistant/app/feature/paywall/EntitlementStore.kt index 54c0b75..a06ad5d 100644 --- a/app/src/main/java/in/artistant/app/feature/paywall/EntitlementStore.kt +++ b/app/src/main/java/in/artistant/app/feature/paywall/EntitlementStore.kt @@ -13,7 +13,7 @@ import javax.inject.Singleton * Queries Play Billing when [AppEnvironment.subscriptionsEnabled]; otherwise inert. */ @Singleton -class EntitlementStore { +open class EntitlementStore { private val billing: PlayBillingService? @Inject @@ -29,7 +29,7 @@ class EntitlementStore { private val _isEntitled = MutableStateFlow(false) val isEntitled: StateFlow = _isEntitled.asStateFlow() - val subscriptionsActive: Boolean get() = AppEnvironment.subscriptionsEnabled + open val subscriptionsActive: Boolean get() = AppEnvironment.subscriptionsEnabled fun isEntitled(productId: String): Boolean = subscriptionsActive && _isEntitled.value diff --git a/app/src/test/java/in/artistant/app/feature/booking/CheckoutViewModelLogicTest.kt b/app/src/test/java/in/artistant/app/feature/booking/CheckoutViewModelLogicTest.kt index 75acb17..113fd40 100644 --- a/app/src/test/java/in/artistant/app/feature/booking/CheckoutViewModelLogicTest.kt +++ b/app/src/test/java/in/artistant/app/feature/booking/CheckoutViewModelLogicTest.kt @@ -76,4 +76,31 @@ class CheckoutViewModelLogicTest { assertEquals(BookingStatus.PendingConfirm, stored?.status) assertEquals(null, draftStore.draft.value) } + + @Test + fun sendRequest_gatesToPaywallWhenSubscriptionsOnAndNotEntitled() = runTest { + val draftStore = BookingDraftStore() + draftStore.setDraft(draft()) + val bookings = FakeBookingsRepository() + + val entitlements = object : EntitlementStore() { + override val subscriptionsActive: Boolean get() = true + } + + val vm = CheckoutViewModel( + draftStore = draftStore, + artistsRepository = FakeArtistsRepository(), + bookingsRepository = bookings, + paymentsService = MockPaymentsService(), + entitlements = entitlements, + ) + advanceUntilIdle() + + vm.sendRequest() + advanceUntilIdle() + + assertEquals(true, vm.state.value.needsPaywall) + assertEquals(null, vm.state.value.confirmedBookingId) + assertEquals(0, bookings.listForClient().size) + } } diff --git a/app/src/test/java/in/artistant/app/feature/messages/MessagesFilterLogicTest.kt b/app/src/test/java/in/artistant/app/feature/messages/MessagesFilterLogicTest.kt new file mode 100644 index 0000000..0ed77e8 --- /dev/null +++ b/app/src/test/java/in/artistant/app/feature/messages/MessagesFilterLogicTest.kt @@ -0,0 +1,31 @@ +package `in`.artistant.app.feature.messages + +import `in`.artistant.app.data.model.Thread +import org.junit.Assert.assertEquals +import org.junit.Test + +class MessagesFilterLogicTest { + + private fun item(id: String, bookingId: String?) = ThreadListItem( + thread = Thread(id = id, artistId = "a1", bookingId = bookingId), + counterpartName = "Name", + ) + + @Test + fun visibleThreads_partitionsAllBookingsInquiries() { + val threads = listOf( + item("t1", bookingId = "b1"), + item("t2", bookingId = null), + item("t3", bookingId = "b2"), + ) + assertEquals(3, MessagesUiState(threads = threads, filter = MessagesFilter.All).visibleThreads.size) + assertEquals( + listOf("t1", "t3"), + MessagesUiState(threads = threads, filter = MessagesFilter.Bookings).visibleThreads.map { it.thread.id }, + ) + assertEquals( + listOf("t2"), + MessagesUiState(threads = threads, filter = MessagesFilter.Inquiries).visibleThreads.map { it.thread.id }, + ) + } +} diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index 7b26852..9138c61 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -167,24 +167,25 @@ socials/bio) can be built before F8 lands.* ## F11 — Bookings list + detail *(SHARED)* -- [x] **BookingsViewModel + BookingsScreen** (CLIENT) — schedule list; +- [x] **BookingsViewModel + BookingsScreen** (CLIENT) — schedule list + MonthDayGrid; `pendingBookingDetail` deep link via TabRouter. **M.** -- [ ] **ArtistGigsScreen** (ARTIST) — `MonthCalendar` of gigs. **S.** -- [x] **BookingDetailScreen** (SHARED) — timeline, KV, actions (cancel/accept/decline); - message/review/calendar deferred. **M.** -- [ ] **MonthCalendar / MiniMonthCalendar / DateScroller** components. **partial** — - `MonthCalendarHeader` + date chips shipped; full grid deferred. **L.** +- [x] **ArtistGigsScreen** (ARTIST) — MonthDayGrid + month-grouped list. **S.** +- [x] **BookingDetailScreen** (SHARED) — Accept/Decline, Message, Getting there, + Add to calendar, Leave a review. **M.** +- [x] **MonthDayGrid** — year-month scoped busy/filter day cells. **M.** --- ## F12 — Artist home + EPK *(ARTIST)* -- [ ] **ArtistHomeViewModel + Screen** — earnings `Sparkline`, bookability card, - 14-day availability strip, requests, upcoming, upload banner, subscribe banner. - **partial:** New requests rail + Score CTA shipped; sparkline/busy strip deferred. +- [x] **ArtistHomeViewModel + Screen** — earnings Sparkline, 14-day busy strip, + New requests, open quotes, Up next (+ Score CTA). **L.** - [x] **EpkViewModel + EpkScreen** — packages replace, tech, links CRUD, samples add/delete. **partial:** photo grid reorder / share-link deferred. - [x] **ManageAvailabilityScreen** — days/times chips + save. **S.** +- [x] **CommunityCommitmentScreen** — ACCT-05 pledge before Role. **S.** +- [x] **Avatar + HeaderBar** design components. **S.** +- [x] **Messages filters** — All / Bookings / Inquiries. **S.** ## F13 — Bookability score *(mostly ARTIST)* @@ -204,7 +205,7 @@ socials/bio) can be built before F8 lands.* ## F15 — Profile, settings, DPDP, calendar sync -- [x] **ProfileScreen** — identity header + settings rows (sign out, delete, export, privacy/help) + calendar sync toggle. **M.** Stats/saved carousel deferred. +- [x] **ProfileScreen** — identity header + settings rows (sign out, delete, export, privacy/help) + calendar sync toggle + Bookings/Saved/Completed stats → ArtistList. **M.** - [x] **Sign out** — `SessionManager.signOut` + prefs wipe; RootViewModel routes to auth. **S.** - [x] **Data export** — `data-export` EF → share sheet (inline) or browser (signed URL). **S.** - [x] **Delete account** — confirm dialog → `delete-account` EF → calendar wipe + signOut. **S.** diff --git a/docs/PARITY_CHECKLIST.md b/docs/PARITY_CHECKLIST.md index bae86e1..603c618 100644 --- a/docs/PARITY_CHECKLIST.md +++ b/docs/PARITY_CHECKLIST.md @@ -12,9 +12,8 @@ Airbnb-style trust (safety banner + “always communicate through Artistant” + report). Schema through ~0085. Android must not rebuild the retired redaction moat. -**Android current:** M0–M1 complete; **M2–M7 on `main` (PR #44)** plus -`feature/parity-polish` closing remaining partials (BookingDetail CTAs, Artist -Home dashboard, Messages filters, MonthDayGrid, Help/Feedback, SearchRecents). +**Android current:** M0–M7 + parity polish on `main` (PR #44 + PR #45). Product +truth: redaction retired, request→accept, Airbnb chat trust. --- @@ -208,7 +207,7 @@ Home dashboard, Messages filters, MonthDayGrid, Help/Feedback, SearchRecents). | `Components/Skeleton.swift` | `designsystem/component/Skeleton.kt` | done | | | `Components/ScoreRing.swift` | `designsystem/component/ScoreRing.kt` | done | New-tier nil handling | | `Components/Sparkline` | `designsystem/component/Sparkline.kt` | done | | -| `Components/HeaderBar.swift` | — | missing | | +| `Components/HeaderBar.swift` | `designsystem/component/HeaderBar.kt` | done | | | Theme tokens | `designsystem/theme/*` | done | Brand fonts TTF drop still operator (#15) | --- @@ -219,6 +218,9 @@ Home dashboard, Messages filters, MonthDayGrid, Help/Feedback, SearchRecents). - Operator Google/Apple dashboard config + `google-services.json` / FCM server path - ExoPlayer sample / Spotify embed playback - Artist profile PROF-* Airbnb extras (hero pager, review search/sort) +- M8 instrumented UI / Play upload / release packaging +- Flip `subscriptionsEnabled` + Play RTDN when IAP goes live +- Observability keys (PostHog / Sentry) when ready to go live ---