Compare commits

..

No commits in common. "523c7120b8c135d633efba767bcd14c1dda47da9" and "b17271c89fa7d165cebcb1c9b9034868e41c2a13" have entirely different histories.

15 changed files with 539 additions and 1475 deletions

View File

@ -316,7 +316,7 @@ private fun BiometricLockScreen(activity: AppCompatActivity, onUnlocked: () -> U
}
).authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock Closer Couples")
.setTitle("Unlock Closer")
.setSubtitle("Verify it's you to continue")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or

View File

@ -36,11 +36,11 @@ object CloserCopy {
object Paywall {
const val headline = "Go deeper together"
const val subhead = "Unlock everything Closer Couples has built for you."
const val subhead = "Unlock everything Closer has built for couples."
const val choosePlanTitle = "Choose your plan"
const val coupleShared = "One subscription covers you both — treat your partner."
const val thankYouTitle = "You're all set"
const val thankYouBody = "Thank you for supporting Closer Couples."
const val thankYouBody = "Thank you for supporting Closer."
}
object Subscription {
@ -53,9 +53,9 @@ object CloserCopy {
* error was triplicated verbatim. Two disclaimer variants are intentionally distinct
* (brief on sign-up, with-reason on the DOB step) kept separate, not unified. */
object AgeGate {
const val disclaimer = "Closer Couples is an 18+ app."
const val disclaimerWithReason = "Closer Couples is an 18+ app. We ask once to confirm your age."
const val disclaimer = "Closer is an 18+ app."
const val disclaimerWithReason = "Closer is an 18+ app. We ask once to confirm your age."
const val dobRequired = "Please enter your date of birth."
fun ageError(minAge: Int) = "You must be at least $minAge to use Closer Couples."
fun ageError(minAge: Int) = "You must be at least $minAge to use Closer."
}
}

View File

@ -10,7 +10,7 @@ import app.closer.core.navigation.JoinLink
object InviteShareText {
fun build(code: String): String {
val pretty = code.chunked(3).joinToString(" ")
return "Let's try Closer Couples together 💜 It's our own private space to actually talk. " +
return "Let's try Closer together 💜 It's our own private space to actually talk. " +
"Tap to join me: ${JoinLink.webUrl(code)}\n\n" +
"Or enter this code in the app: $pretty"
}

View File

@ -102,7 +102,7 @@ fun PairPromptScreen(
Spacer(Modifier.height(16.dp))
Text(
text = "Closer Couples is built for two. Connect your partner to unlock everything worth unlocking:",
text = "Closer is built for two. Connect your partner to unlock everything worth unlocking:",
style = MaterialTheme.typography.bodyLarge,
color = AuthMuted,
textAlign = TextAlign.Center

View File

@ -34,7 +34,7 @@ object WeeklyRecapShareCard {
else -> "We answered $q questions"
}
val reveals = if (r > 0) " and opened $r reveal${if (r == 1) "" else "s"}" else ""
return "$answered$reveals together this week on Closer Couples 💜"
return "$answered$reveals together this week on Closer 💜"
}
/** Renders the card to a bitmap using plain Canvas (no Compose-capture fragility). */

View File

@ -499,7 +499,7 @@ fun SettingsScreen(
SettingsRow(
icon = CloserGlyphs.Palette,
label = "Appearance",
subtitle = "Make Closer Couples feel comfortable to open",
subtitle = "Make Closer feel comfortable to open",
onClick = { onNavigate(AppRoute.APPEARANCE) }
)
}
@ -534,7 +534,7 @@ fun SettingsScreen(
SettingsRow(
icon = ImageVector.vectorResource(R.drawable.glyph_delete_account),
label = "Delete account",
subtitle = "Permanently remove your Closer Couples account",
subtitle = "Permanently remove your Closer account",
onClick = { onNavigate(AppRoute.DELETE_ACCOUNT) },
tint = SettingsDanger
)

View File

@ -92,9 +92,9 @@ private fun TodayWidgetContent(context: Context, snapshot: TodayWidgetSnapshot)
/** Content-free headline derived only from state flags. */
internal fun headlineFor(s: TodayWidgetSnapshot): String = when {
!s.signedIn -> "Open Closer Couples"
!s.signedIn -> "Open Closer"
!s.paired -> "Invite your partner to begin"
s.appLockEnabled -> "Something's waiting in Closer Couples"
s.appLockEnabled -> "Something's waiting in Closer"
else -> when (s.dailyState) {
"USER_ANSWERED_PARTNER_PENDING" -> "Waiting for your partner"
"PARTNER_ANSWERED_USER_PENDING" -> "Your turn — answer to reveal"

View File

@ -72,8 +72,8 @@
<string name="privacy_streak">Streak and shared wins</string>
<string name="privacy_streak_body">Your couple\'s streak count and challenge completions are shared between both of you.</string>
<string name="privacy_no_export">No data export</string>
<string name="privacy_no_export_body">Closer Couples does not currently offer a data export. Your answers and history are deleted along with your account.</string>
<string name="privacy_rule_intro">Closer Couples is built on one rule: answers stay private until both of you have answered. Here\'s exactly what that means.</string>
<string name="privacy_no_export_body">Closer does not currently offer a data export. Your answers and history are deleted along with your account.</string>
<string name="privacy_rule_intro">Closer is built on one rule: answers stay private until both of you have answered. Here\'s exactly what that means.</string>
<string name="privacy_usage_data_section">Anonymous usage data</string>
<string name="privacy_usage_data_title">Share anonymous usage data</string>
<string name="privacy_usage_data_body">Helps us see which features couples use — never what you write. Your answers, messages, and prompts are end-to-end encrypted and can\'t be in this data even if we wanted them.</string>

View File

@ -15,7 +15,7 @@ class TodayWidgetTest {
@Test
fun `signed-out shows a neutral open prompt`() {
assertEquals("Open Closer Couples", headlineFor(snap(signedIn = false)))
assertEquals("Open Closer", headlineFor(snap(signedIn = false)))
}
@Test
@ -26,7 +26,7 @@ class TodayWidgetTest {
@Test
fun `app-lock hides the daily state behind a generic headline`() {
val locked = headlineFor(snap(appLock = true, state = "BOTH_ANSWERED"))
assertEquals("Something's waiting in Closer Couples", locked)
assertEquals("Something's waiting in Closer", locked)
// The specific state must NOT leak when the lock is on.
assertFalse(locked.contains("reveal", ignoreCase = true))
}

View File

@ -1,15 +1,7 @@
# App rename: "Closer" → "Closer Couples"
# App rename: "Closer" → "Closer Couples" (store / marketing tier only)
> Audit + decision record for renaming the product's public name to **"Closer Couples"**.
> Status: **executed in repo.** Store-console changes remain external owner steps.
>
> **2026-07-09 extension — in-app prose now renamed.** The original pass was store/marketing only
> (in-app copy deliberately stayed "Closer"). Owner decision to extend: **in-app *prose* copy now uses
> "Closer Couples"** across Android + iOS (settings, pairing, invite share, widget headlines, paywall
> subhead/thank-you, age gate, privacy body, iOS settings/onboarding/pairing). The **launcher label and
> wordmark logos still stay "Closer"** (owner kept these — truncation under the icon, and the logo is a
> brand mark). See "What still stays 'Closer'" below. No technical identifiers touched; Android compiles +
> unit tests green (`TodayWidgetTest` assertions updated).
## Context
@ -25,20 +17,12 @@ After auditing every occurrence of the name across Android, iOS, images, and doc
Renaming any of these breaks builds / Firebase / the Play listing:
`namespace app.closer` · `applicationId closer.app` · package `app.closer.*` + `class CloserApp` · deep-link host `closer.app` + `closer://` scheme · Firebase `closer-app-22014` (`google-services.json`, `GoogleService-Info.plist`) · iOS `PRODUCT_BUNDLE_IDENTIFIER app.closer.iphone` + `bundleIdPrefix app.closer` + product/target/scheme `Closer` · SPM package `Closer` · Gradle `rootProject.name "Closer"` · resource filenames (`closer_launcher_*`, `ic_notification_closer`, `ic_launcher*`) · XML styles `Theme.Closer[.Splash]` · entitlement id `closer_premium` · crypto AAD constants.
## What still stays "Closer" (after the 2026-07-09 prose extension)
## What does NOT change (per the chosen tier)
- **Android launcher label**`app/src/main/res/values/strings.xml``app_name` stays **"Closer"** (14-char "Closer Couples" truncates under the icon).
- **Android launcher label**`app/src/main/res/values/strings.xml``app_name` stays **"Closer"**.
- **iOS home-screen label**`iphone/Closer/Info.plist``CFBundleDisplayName` stays **"Closer"** (`CFBundleName`/`PRODUCT_NAME` untouched).
- **Wordmark logos** (onboarding `OnboardingScreen`/iOS `OnboardingViews`, auth alt-text `AuthVisuals`, `TodayWidget` header, `WeeklyRecapShareCard` drawText) stay "Closer" → the logo is a brand mark; their asserting tests (`FirstRunRenderSmokeTest` contentDescription, recap/widget tests) stay green.
- **Deliberate copy exceptions that stay "Closer":** the **"Grow Closer"** onboarding headline (intimacy pun, not the app name); the privacy-rotator slogan *"…so Closer cannot read them"* and iOS *"not even Closer's servers"* (entity references; the rotator line also has a hard 64-char display cap that "Closer Couples" would exceed — `CloserBrandCopyTest`); the iOS **"Settings > Closer"** help path (must match the kept `CFBundleDisplayName`); and the **"Closer Premium"** tier label (matches the RevenueCat entitlement display name).
- **Internal engineering/brand-system docs** using "Closer" as codename/design-system name (`DEVELOPMENT_LOG.md`, `SECURITY.md`, `Future.md`, `Engineering_Reference_Manual`, `Claude*.md`, brand-system docs).
## In-app prose renamed to "Closer Couples" (2026-07-09)
The ~26 user-facing strings where "Closer" names the app in a sentence now read "Closer Couples":
- **Android (9 files):** `CloserCopy` (paywall subhead reworded to avoid "Couples…couples", thank-you, age-gate ×3), `InviteShareText`, `PairPromptScreen`, `SettingsScreen` (appearance + delete-account subtitles), `TodayWidget` (Open / "Something's waiting" headlines — header wordmark kept), `MainActivity` biometric prompt, `WeeklyRecapShareCard` caption (drawText wordmark kept), `strings.xml` (`privacy_no_export_body`, `privacy_rule_intro`). `TodayWidgetTest` exact-match assertions updated.
- **iOS (2 files):** `SettingsViews` (profile/account/data-export/legal/help/premium-eligibility/paywall subhead), `PairingViews` explore line.
- **All ~25 in-app copy strings** stay "Closer" (paywall, onboarding/age-gate, settings, widget, invite share text, privacy `strings.xml:95-96`, share card, iOS onboarding, etc.).
- **Wordmark logos** (onboarding, auth alt-text, widget, share card) stay "Closer" → their asserting tests stay green, untouched.
- **Internal engineering/brand-system docs** using "Closer" as codename/design-system name (`DEVELOPMENT_LOG.md`, `SECURITY.md`, `Future.md`, `Engineering_Reference_Manual`, the `Claude*.md` reports, `docs/brand/asset-system.md` "Closer Artwork Asset System", `docs/brand/generated-art/glyphs/README.md` "Closer G-set").
## Changes to make (in-repo)

View File

@ -28,7 +28,7 @@ struct PairPromptView: View {
)
.closerPadding()
Text("You can keep exploring Closer Couples, but shared reveals and partner activity start after your person joins.")
Text("You can keep exploring Closer, but shared reveals and partner activity start after your person joins.")
.font(CloserFont.footnote)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)

View File

@ -28,7 +28,7 @@ struct SettingsView: View {
SettingsProfileHeader(
initials: initials,
name: appState.currentUser?.displayName ?? "You",
detail: "Create an account to save your Closer Couples space",
detail: "Create an account to save your Closer space",
imageUrl: appState.currentUser?.photoUrl
)
}
@ -54,7 +54,7 @@ struct SettingsView: View {
SettingsLinkRow(
icon: "heart.fill",
title: "Connected with \(partner.displayName.isEmpty ? "Partner" : partner.displayName)",
subtitle: "Your shared Closer Couples space is active",
subtitle: "Your shared Closer space is active",
tint: .closerPrimary,
imageUrl: partner.photoUrl,
showsChevron: false
@ -124,7 +124,7 @@ struct SettingsView: View {
SettingsLinkRow(
icon: "square.and.arrow.up",
title: "Export Data",
subtitle: "Download a copy of your Closer Couples data"
subtitle: "Download a copy of your Closer data"
)
}
.buttonStyle(.plain)
@ -150,7 +150,7 @@ struct SettingsView: View {
SettingsLinkRow(
icon: "doc.text",
title: "Terms of Service",
subtitle: "The agreement for using Closer Couples"
subtitle: "The agreement for using Closer"
)
}
.buttonStyle(.plain)
@ -177,7 +177,7 @@ struct SettingsView: View {
SettingsLinkRow(
icon: "envelope",
title: "Contact Us",
subtitle: "Get help from the Closer Couples team"
subtitle: "Get help from the Closer team"
)
}
.buttonStyle(.plain)
@ -207,7 +207,7 @@ struct SettingsView: View {
SettingsLinkRow(
icon: "trash",
title: "Delete Account",
subtitle: "Permanently remove your Closer Couples account",
subtitle: "Permanently remove your Closer account",
tint: .closerDanger,
isDestructive: true,
showsChevron: false
@ -407,7 +407,7 @@ struct DataExportView: View {
.font(CloserFont.title2)
.foregroundColor(.closerText)
Text("Download a copy of all your Closer Couples data including answers, memories, and preferences.")
Text("Download a copy of all your Closer data including answers, memories, and preferences.")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
@ -580,7 +580,7 @@ struct PairingHelpView: View {
BulletPoint("Codes expire after a set time — generate a new one if needed")
BulletPoint("Each account can only be in one couple at a time")
BulletPoint("Both of you need a Closer Couples account first")
BulletPoint("Both of you need a Closer account first")
}
.closerPadding()
}
@ -704,7 +704,7 @@ struct PaywallView: View {
.foregroundColor(.closerText)
.multilineTextAlignment(.center)
Text("Unlock everything Closer Couples has built for you.")
Text("Unlock everything Closer has built for couples.")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)

View File

@ -1,4 +1,4 @@
# Fun Relationship Questions Research Notes v2
# Fun Relationship Questions Research Notes v1
This research note updates the Closer daily question guides after reviewing modern couple apps, conversation-card games, date-night question lists, and relationship research summaries.
@ -64,21 +64,3 @@ Copy these patterns:
* GQ on Esther Perel's conversation game: https://www.gq.com/story/esther-perel-card-game
* Verywell Mind spicy questions: https://www.verywellmind.com/spicy-conversation-starters-8737188
* Partnership through Play research: https://arxiv.org/abs/2505.09509
## Gap Found After v6
The v6 guide improved fun, but still left two gaps.
First, it did not force patch discipline strongly enough. A writer could mark a few weak questions and then rewrite far more than needed. Future guide versions must require `fix_marked_ids_only` unless a mass rewrite exception is documented.
Second, it did not clearly separate fun from random. A pack can avoid therapy voice but drift into snack overload, fake award overload, or childish nonsense. Future daily packs must stay playful and adult, not random for its own sake.
## Added In v7
* Patch mode is required by default.
* Passing questions must stay frozen.
* Mass rewrites require a documented over-60-percent shared failure.
* Every marked question needs a fix scope.
* Fun but grounded is now a gate.
* The option answer test is now required.
* Remaining hard flags must be 0 before production-ready.

View File

@ -0,0 +1,84 @@
# Fun Relationship Questions Research Notes v2
This research note updates the Closer daily question guides after reviewing modern couple apps, conversation-card games, date-night question lists, and relationship research summaries.
## Main Takeaway
Fun relationship questions work when they feel like a small playable moment.
They are not just "warm prompts". They use choice, humor, tiny missions, preferences, memories, flirtation, and low-pressure honesty.
## What Good Examples Have in Common
* They are quick to answer.
* They create a tiny story or choice.
* They ask about favorites, memories, date ideas, silly preferences, attraction, or what sounds good now.
* They give permission to be playful, weird, honest, or flirty.
* They avoid sounding like therapy unless the product is clearly built for therapy.
* They do not make the relationship feel like work.
## What Closer Should Copy As A Pattern
Do not copy outside questions.
Copy these patterns:
* "Would you rather" choices
* fake awards
* tiny missions
* snack drafts
* date-night picks
* silly debates
* comfort-show or music choices
* flirty low-pressure picks
* first memory or favorite memory prompts
* small adventure choices
* "what should we try tonight" prompts
## What Closer Should Avoid
* chores pretending to be romance
* bedtime logistics
* household admin
* therapy words
* generic wellness phrasing
* vague answers like "quality time"
* awkward domestic answers like "the good blanket saved"
* options that do not all answer the same prompt
## Specific Guide Changes Made
* Added a research-informed fun rules section.
* Added required daily game mechanics.
* Added stronger option checks.
* Added a research pass before writing daily packs.
* Added a second sample gate requiring 8 of 10 sampled questions per weekday to feel fun, sweet, flirty, silly, date-like, or game-like.
* Added metadata fields for `content_policy` and `research_note`.
## Sources Reviewed
* Paired: https://www.paired.com/
* Gottman Love Maps: https://www.gottman.com/blog/the-sound-relationship-house-build-love-maps/
* Teen Vogue couples questions: https://www.teenvogue.com/story/145-questions-for-couples-that-are-always-worth-asking
* Guardian on conversation cards: https://www.theguardian.com/wellness/2024/feb/23/conversation-starters-card-games
* GQ on Esther Perel's conversation game: https://www.gq.com/story/esther-perel-card-game
* Verywell Mind spicy questions: https://www.verywellmind.com/spicy-conversation-starters-8737188
* Partnership through Play research: https://arxiv.org/abs/2505.09509
## Gap Found After v6
The v6 guide improved fun, but still left two gaps.
First, it did not force patch discipline strongly enough. A writer could mark a few weak questions and then rewrite far more than needed. Future guide versions must require `fix_marked_ids_only` unless a mass rewrite exception is documented.
Second, it did not clearly separate fun from random. A pack can avoid therapy voice but drift into snack overload, fake award overload, or childish nonsense. Future daily packs must stay playful and adult, not random for its own sake.
## Added In v7
* Patch mode is required by default.
* Passing questions must stay frozen.
* Mass rewrites require a documented over-60-percent shared failure.
* Every marked question needs a fix scope.
* Fun but grounded is now a gate.
* The option answer test is now required.
* Remaining hard flags must be 0 before production-ready.

File diff suppressed because it is too large Load Diff