Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.)

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +29,7 @@ class EntitlementStore {
private val _isEntitled = MutableStateFlow(false)
val isEntitled: StateFlow<Boolean> = _isEntitled.asStateFlow()

val subscriptionsActive: Boolean get() = AppEnvironment.subscriptionsEnabled
open val subscriptionsActive: Boolean get() = AppEnvironment.subscriptionsEnabled

fun isEntitled(productId: String): Boolean =
subscriptionsActive && _isEntitled.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 },
)
}
}
21 changes: 11 additions & 10 deletions docs/FEATURE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)*

Expand All @@ -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.**
Expand Down
10 changes: 6 additions & 4 deletions docs/PARITY_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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) |

---
Expand All @@ -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

---

Expand Down