mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f89b1345 | ||
|
|
8804e121b6 | ||
|
|
7b3de48dbd | ||
|
|
5ebf57bdbf | ||
|
|
089e629021 | ||
|
|
58b70710db | ||
|
|
d645d83c47 | ||
|
|
179f912170 | ||
|
|
a604f82110 | ||
|
|
ae8e72ddb5 | ||
|
|
2e4cddd1c8 | ||
|
|
440f224095 | ||
|
|
22871d037c | ||
|
|
fd38b303f0 | ||
|
|
266233a473 | ||
|
|
387b242aea | ||
|
|
2bc69c2790 | ||
|
|
9da44217f7 |
No files matched your search
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
name: generate-release
|
||||||
|
description: Generate a signed Sauci mobile release by bumping the public version, writing on-brand release notes, building and verifying iOS and Android binaries, committing release metadata, and tagging the release commit. Use for an actual production mobile release; use release-mobile for preparation, one-platform builds, uploads, or submission work that does not require the complete release lifecycle.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Generate release
|
||||||
|
|
||||||
|
Create a reproducible release whose tag identifies the reviewed release source
|
||||||
|
and final platform build metadata. Read `apps/mobile/AGENTS.md`,
|
||||||
|
`docs/releasing.md`, and the `release-mobile` and `commit` skills before acting. Read
|
||||||
|
[references/release-notes-voice.md](references/release-notes-voice.md) before
|
||||||
|
drafting notes and [references/artifact-verification.md](references/artifact-verification.md)
|
||||||
|
before accepting a build.
|
||||||
|
|
||||||
|
A request to generate or create a release authorizes the scoped version edits,
|
||||||
|
release notes, local builds, release commits, and one local annotated tag. It
|
||||||
|
does not authorize pushing commits or tags, uploading binaries, submitting a
|
||||||
|
store release, changing signing credentials, or regenerating native projects.
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
- Build both iOS and Android production binaries unless the user narrows the
|
||||||
|
platforms.
|
||||||
|
- Increment the patch version unless the user specifies `minor`, `major`, or an
|
||||||
|
exact SemVer version.
|
||||||
|
- Use `v<version>` for the annotated Git tag.
|
||||||
|
- Write artifacts beneath ignored `dist/releases/v<version>/`; never commit
|
||||||
|
binaries, credentials, logs, or build directories.
|
||||||
|
|
||||||
|
## Preflight
|
||||||
|
|
||||||
|
Fetch the remote and inspect the worktree, branch, upstream, existing tags,
|
||||||
|
toolchains, EAS authentication, and signing availability without changing them.
|
||||||
|
Require all of the following:
|
||||||
|
|
||||||
|
- a named branch, normally `main` for a production release;
|
||||||
|
- a clean starting worktree with `HEAD` equal to its fetched upstream;
|
||||||
|
- the previous reachable `v[0-9]*` release tag and no collision for the next tag;
|
||||||
|
- consistent public versions across `app.json`, Android `versionName`, and every
|
||||||
|
iOS `MARKETING_VERSION` (`scripts/set_version.mjs --check`);
|
||||||
|
- Node 20, JDK 17 for Android, and Xcode/CocoaPods for iOS;
|
||||||
|
- the configured production API/Auth/RevenueCat environment, without printing
|
||||||
|
secret values.
|
||||||
|
|
||||||
|
Do not switch branches, discard work, repair credentials, push, or weaken branch
|
||||||
|
protection merely to pass preflight. Stop with the exact blocker. Never run
|
||||||
|
`expo prebuild` as part of this workflow.
|
||||||
|
|
||||||
|
## Prepare the release
|
||||||
|
|
||||||
|
1. Derive the next version with `scripts/set_version.mjs --bump <kind>` or set an
|
||||||
|
explicit version with `--version <x.y.z>`. The script updates the checked-in
|
||||||
|
public-version sources but deliberately leaves platform build numbers to EAS.
|
||||||
|
2. Review the complete first-parent and ordinary commit diff from the previous
|
||||||
|
reachable release tag to the starting commit. Inspect the actual product diff
|
||||||
|
for every user-facing claim; commit subjects alone are not evidence.
|
||||||
|
3. Add the new entry immediately below the title in `docs/release-notes.md`.
|
||||||
|
Describe user-visible value, meaningful fixes, privacy or reliability changes,
|
||||||
|
and only claims supported by the diff. Omit internal migrations, refactors,
|
||||||
|
CI, infrastructure, dependency churn, and implementation jargon unless the
|
||||||
|
user experiences a direct result. Use a temporary `Build numbers pending
|
||||||
|
artifact verification` line until the binaries have been inspected; never
|
||||||
|
guess or predict build numbers.
|
||||||
|
4. Run `npm run verify:full` and the native acceptance required by the
|
||||||
|
`release-mobile` skill. A missing prerequisite is a failure/limitation, not a
|
||||||
|
pass.
|
||||||
|
5. Review the scoped diff and create `chore(release): prepare v<version>`. Do not
|
||||||
|
stage unrelated paths. This clean release-preparation commit must descend
|
||||||
|
directly from the fetched upstream commit.
|
||||||
|
|
||||||
|
## Build and capture platform metadata
|
||||||
|
|
||||||
|
Build from the clean prepared source using the package scripts in
|
||||||
|
`apps/mobile/package.json`, with `EAS_LOCAL_BUILD_ARTIFACTS_DIR` set to the
|
||||||
|
versioned artifact directory. When building both platforms, build Android first,
|
||||||
|
then iOS.
|
||||||
|
|
||||||
|
After each build:
|
||||||
|
|
||||||
|
1. Inspect the worktree before doing anything else. Accept only the documented
|
||||||
|
EAS build-number changes in `app.json`, that platform's native version source,
|
||||||
|
and the iOS widget plist when applicable. Treat any implementation, resource,
|
||||||
|
dependency, or native-project regeneration as a failed build.
|
||||||
|
2. Independently inspect the binary for package/bundle identifier, public
|
||||||
|
version, platform build number, production endpoint configuration where
|
||||||
|
inspectable, and signing identity/fingerprint. Require the embedded public
|
||||||
|
version to equal the requested version.
|
||||||
|
3. Record the verified platform build number in the new release-note entry.
|
||||||
|
4. Commit only the expected metadata and release-note update as
|
||||||
|
`chore(<platform>): record v<version> build <number>`. The worktree must be
|
||||||
|
clean before starting the next platform build.
|
||||||
|
|
||||||
|
Do not accept a successful EAS exit code as proof of a valid artifact. If either
|
||||||
|
requested platform fails, do not tag a partial release.
|
||||||
|
|
||||||
|
## Tag and prove the result
|
||||||
|
|
||||||
|
Re-run focused version checks and `npm run verify:fast`. Confirm every requested
|
||||||
|
artifact exists, has a SHA-256 digest, passed independent version/signature
|
||||||
|
inspection, and was produced from the release commit chain. Require a clean
|
||||||
|
worktree, then create the annotated local tag `v<version>` at `HEAD`.
|
||||||
|
|
||||||
|
Read back the tag object and prove:
|
||||||
|
|
||||||
|
- the tag resolves to `HEAD`;
|
||||||
|
- the tag version equals all public-version sources and embedded artifacts;
|
||||||
|
- recorded build numbers equal the embedded platform build numbers;
|
||||||
|
- no binary or ignored build output entered the commit;
|
||||||
|
- the release commits descend from the fetched starting branch commit;
|
||||||
|
- for a two-platform release, the Android artifact's source commit differs from
|
||||||
|
the final tag only by the later verified iOS build metadata.
|
||||||
|
|
||||||
|
Report the branch, tag, commit, release-note summary, artifact paths, versions,
|
||||||
|
build numbers, signatures, SHA-256 digests, checks run, and any environmental
|
||||||
|
limitations. State explicitly that the tag and commits remain local unless a
|
||||||
|
separate authorized push was completed.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Generate Release"
|
||||||
|
short_description: "Bump, build, note, and tag Sauci releases"
|
||||||
|
default_prompt: "Use $generate-release to create the next signed Sauci mobile release for iOS and Android."
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Release artifact verification
|
||||||
|
|
||||||
|
Use the strongest locally available platform tools and record the commands and
|
||||||
|
results. Never print provisioning profiles, keys, tokens, or environment values.
|
||||||
|
|
||||||
|
## Android AAB
|
||||||
|
|
||||||
|
Require a production `.aab`. Inspect its manifest with `bundletool dump manifest`
|
||||||
|
or Android SDK `apkanalyzer`, and verify:
|
||||||
|
|
||||||
|
- package `com.sauci.app`;
|
||||||
|
- `versionName` equals the release version;
|
||||||
|
- `versionCode` equals the recorded Android build number.
|
||||||
|
|
||||||
|
Verify the archive signature with `jarsigner -verify -verbose -certs`. Compare the
|
||||||
|
signing certificate SHA-1 to the production fingerprint documented in
|
||||||
|
`docs/releasing.md`; do not copy credentials into commands or reports. Calculate
|
||||||
|
the artifact SHA-256 with `shasum -a 256`.
|
||||||
|
|
||||||
|
## iOS IPA
|
||||||
|
|
||||||
|
Extract the `.ipa` into a new temporary directory created with `mktemp -d`. Read
|
||||||
|
the application `Info.plist` with `/usr/libexec/PlistBuddy` or `plutil` and verify:
|
||||||
|
|
||||||
|
- `CFBundleIdentifier` is `com.sauci.app`;
|
||||||
|
- `CFBundleShortVersionString` equals the release version;
|
||||||
|
- `CFBundleVersion` equals the recorded iOS build number.
|
||||||
|
|
||||||
|
Use `codesign -dvvv` and `codesign --verify --deep --strict` on the extracted app
|
||||||
|
to inspect and verify signing without exposing the embedded profile. Calculate the
|
||||||
|
original `.ipa` SHA-256 with `shasum -a 256`. Remove only the explicit temporary
|
||||||
|
extraction directory after inspection.
|
||||||
|
|
||||||
|
## Source correspondence
|
||||||
|
|
||||||
|
For both platforms, retain the build command, artifact path, build completion
|
||||||
|
time, and digest in the final report. Compare the post-build tracked diff with the
|
||||||
|
documented allowed version files. A binary with the right version but unexpected
|
||||||
|
source regeneration, wrong signing, or an unverifiable manifest is not accepted.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Sauci release-note voice
|
||||||
|
|
||||||
|
Sauci sounds warm, confident, inviting, and quietly playful. It is a premium
|
||||||
|
couples app about private connection and shared curiosity: supportive without
|
||||||
|
being clinical, polished without sounding corporate, and intimate without being
|
||||||
|
graphic or presumptuous.
|
||||||
|
|
||||||
|
## Shape
|
||||||
|
|
||||||
|
Use a short opening sentence followed by three to six compact bullets. Lead with
|
||||||
|
the change that most improves a couple's experience. Combine related work into
|
||||||
|
one benefit rather than mirroring the commit list. Include a brief closing line
|
||||||
|
only when it adds warmth or useful context.
|
||||||
|
|
||||||
|
In `docs/release-notes.md`, use:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## v1.2.3 — 29 August 2026
|
||||||
|
_iOS build 44 · Android build 48_
|
||||||
|
|
||||||
|
A little more ease, clarity, and connection in every check-in.
|
||||||
|
|
||||||
|
- **Closer conversations** — Find the prompts that feel right for both of you.
|
||||||
|
- **Smoother moments** — Chats and matches now feel quicker and more reliable.
|
||||||
|
- **More control** — Clearer choices help keep your shared space comfortable.
|
||||||
|
```
|
||||||
|
|
||||||
|
Adapt the words to the evidence; do not reuse this copy when those claims are not
|
||||||
|
present in the release. If only one platform was requested, record only its build.
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
- Address the couple's experience with `you`, `your partner`, `together`, or
|
||||||
|
`your shared space` where natural.
|
||||||
|
- Prefer concrete benefits: `pick up where you left off`, `find the right prompt`,
|
||||||
|
`keep conversations flowing`, `stay in control`.
|
||||||
|
- Use restrained warmth. One gentle phrase is enough; avoid forced innuendo,
|
||||||
|
breathless hype, exclamation marks, emoji, and claims that Sauci will improve or
|
||||||
|
fix a relationship.
|
||||||
|
- Prefer `new`, `smoother`, `clearer`, `more private`, and `more reliable` to
|
||||||
|
`revolutionary`, `game-changing`, `massive`, or `best ever`.
|
||||||
|
- Explain a fix as the experience restored, not the defect mechanics. Name a
|
||||||
|
security or privacy change plainly when users need to understand it.
|
||||||
|
|
||||||
|
## Evidence boundary
|
||||||
|
|
||||||
|
Release notes describe shipped behavior only. Do not claim a feature from a plan,
|
||||||
|
flagged-off path, migration, test, or commit title without confirming the runtime
|
||||||
|
path in the diff and release configuration. Do not expose internal service names,
|
||||||
|
database details, moderation logic, security-sensitive implementation, or content
|
||||||
|
that the product intentionally keeps private between partners.
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDir, "../../../..");
|
||||||
|
const files = {
|
||||||
|
app: path.join(repoRoot, "apps/mobile/app.json"),
|
||||||
|
android: path.join(repoRoot, "apps/mobile/android/app/build.gradle"),
|
||||||
|
ios: path.join(repoRoot, "apps/mobile/ios/Sauci.xcodeproj/project.pbxproj"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const semverPattern = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/;
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(`set_version: ${message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVersion(value, label) {
|
||||||
|
const match = semverPattern.exec(value);
|
||||||
|
if (!match) fail(`${label} must be an exact numeric SemVer version (x.y.z)`);
|
||||||
|
return match.slice(1).map(Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readState() {
|
||||||
|
const appRaw = fs.readFileSync(files.app, "utf8");
|
||||||
|
const androidRaw = fs.readFileSync(files.android, "utf8");
|
||||||
|
const iosRaw = fs.readFileSync(files.ios, "utf8");
|
||||||
|
const appVersion = JSON.parse(appRaw).expo?.version;
|
||||||
|
const androidVersions = [...androidRaw.matchAll(/\bversionName\s+["']([^"']+)["']/g)].map(
|
||||||
|
(match) => match[1],
|
||||||
|
);
|
||||||
|
const iosVersions = [...iosRaw.matchAll(/\bMARKETING_VERSION\s*=\s*([^;]+);/g)].map(
|
||||||
|
(match) => match[1].trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof appVersion !== "string") fail("apps/mobile/app.json has no expo.version");
|
||||||
|
if (androidVersions.length !== 1) fail("expected exactly one Android versionName");
|
||||||
|
if (iosVersions.length === 0) fail("found no iOS MARKETING_VERSION values");
|
||||||
|
|
||||||
|
const versions = [appVersion, ...androidVersions, ...iosVersions];
|
||||||
|
versions.forEach((version) => parseVersion(version, `configured version ${version}`));
|
||||||
|
if (new Set(versions).size !== 1) {
|
||||||
|
fail(`public versions disagree: ${versions.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { appRaw, androidRaw, iosRaw, current: appVersion, iosCount: iosVersions.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextVersion(current, bump) {
|
||||||
|
const [major, minor, patch] = parseVersion(current, "current version");
|
||||||
|
if (bump === "major") return `${major + 1}.0.0`;
|
||||||
|
if (bump === "minor") return `${major}.${minor + 1}.0`;
|
||||||
|
if (bump === "patch") return `${major}.${minor}.${patch + 1}`;
|
||||||
|
fail("--bump must be patch, minor, or major");
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const check = args.length === 1 && args[0] === "--check";
|
||||||
|
const bumpIndex = args.indexOf("--bump");
|
||||||
|
const versionIndex = args.indexOf("--version");
|
||||||
|
|
||||||
|
if (!check && (bumpIndex === -1) === (versionIndex === -1)) {
|
||||||
|
fail("use --check, --bump patch|minor|major, or --version x.y.z");
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = readState();
|
||||||
|
if (check) {
|
||||||
|
console.log(`public version ${state.current} is consistent across app.json, Android, and ${state.iosCount} iOS settings`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const desired =
|
||||||
|
bumpIndex !== -1
|
||||||
|
? nextVersion(state.current, args[bumpIndex + 1])
|
||||||
|
: (parseVersion(args[versionIndex + 1], "--version"), args[versionIndex + 1]);
|
||||||
|
|
||||||
|
if (desired === state.current) fail(`requested version is already ${state.current}`);
|
||||||
|
const currentParts = parseVersion(state.current, "current version");
|
||||||
|
const desiredParts = parseVersion(desired, "requested version");
|
||||||
|
const increases = desiredParts.some(
|
||||||
|
(part, index) => part > currentParts[index] && desiredParts.slice(0, index).every((value, prior) => value === currentParts[prior]),
|
||||||
|
);
|
||||||
|
if (!increases) fail(`requested version ${desired} must be greater than ${state.current}`);
|
||||||
|
|
||||||
|
const appRaw = state.appRaw.replace(
|
||||||
|
/(\"version\"\s*:\s*\")([^\"]+)(\")/,
|
||||||
|
`$1${desired}$3`,
|
||||||
|
);
|
||||||
|
const androidRaw = state.androidRaw.replace(
|
||||||
|
/(\bversionName\s+["'])([^"']+)(["'])/,
|
||||||
|
`$1${desired}$3`,
|
||||||
|
);
|
||||||
|
const iosRaw = state.iosRaw.replace(
|
||||||
|
/(\bMARKETING_VERSION\s*=\s*)([^;]+)(;)/g,
|
||||||
|
`$1${desired}$3`,
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(files.app, appRaw);
|
||||||
|
fs.writeFileSync(files.android, androidRaw);
|
||||||
|
fs.writeFileSync(files.ios, iosRaw);
|
||||||
|
console.log(`updated public version ${state.current} -> ${desired}`);
|
||||||
@@ -10,14 +10,49 @@ scoped instructions. Confirm target platform and release profile. Run repository
|
|||||||
verification plus a native acceptance pass before starting a release build.
|
verification plus a native acceptance pass before starting a release build.
|
||||||
|
|
||||||
Before a build from the current upstream branch, fetch, require a clean worktree,
|
Before a build from the current upstream branch, fetch, require a clean worktree,
|
||||||
and prove `HEAD` matches its upstream. Sauci tracks its native iOS and Android
|
and prove `HEAD` matches its upstream. The `generate-release` skill may instead
|
||||||
projects, so routine release builds must package those checked-in sources. Do not
|
build a clean, scoped release-preparation commit that descends directly from that
|
||||||
run `expo prebuild` first; native regeneration is a separate reviewed change.
|
fetched upstream; between its multi-platform builds it commits only independently
|
||||||
|
verified EAS version metadata. Sauci tracks its native iOS and Android projects,
|
||||||
|
so routine release builds must package those checked-in sources. Do not run
|
||||||
|
`expo prebuild` first; native regeneration is a separate reviewed change.
|
||||||
|
|
||||||
Use the package scripts in `apps/mobile/package.json`; they create artifacts only.
|
Use the package scripts in `apps/mobile/package.json`; they create artifacts only.
|
||||||
Never infer upload or submission authority from build authorization. Store upload
|
Never infer upload or submission authority from build authorization. Store upload
|
||||||
uses a separately authorized command. Preserve platform signing and local EAS
|
uses a separately authorized command.
|
||||||
versioning. After a build, verify that tracked changes are limited to the expected
|
|
||||||
version metadata, then independently read the artifact version and signature.
|
## Upload authorization gate
|
||||||
Report the artifact, version/build number, upload result when authorized, and
|
|
||||||
read-back state.
|
After an artifact passes verification, stop immediately before the external
|
||||||
|
upload and ask the user for confirmation. The request must identify:
|
||||||
|
|
||||||
|
- the platform, exact artifact path, bundle/package identifier, public version,
|
||||||
|
build number, signature identity, and SHA-256 digest;
|
||||||
|
- the exact destination and resulting state: Google Play internal-track draft or
|
||||||
|
App Store Connect/TestFlight processing;
|
||||||
|
- that the upload does not authorize a public rollout or App Review submission.
|
||||||
|
|
||||||
|
For iOS, inspect the current App Store Connect automatic-distribution behavior
|
||||||
|
before asking. If the build would be distributed automatically, name the exact
|
||||||
|
TestFlight groups or testers in the request and include that effect in the fresh
|
||||||
|
authorization. If the effect cannot be established, stop; changing an automatic-
|
||||||
|
distribution setting also requires separate authorization.
|
||||||
|
|
||||||
|
Even when the original request includes building, releasing, uploading, or
|
||||||
|
submitting, require this fresh confirmation once the exact verified artifact and
|
||||||
|
destination are known. End the request with: `Upload this verified artifact to
|
||||||
|
<destination> now?` A clear affirmative response authorizes one upload of that
|
||||||
|
artifact to that destination, plus only any automatic TestFlight distribution
|
||||||
|
explicitly disclosed in the request. Run `eas submit` with the explicit
|
||||||
|
`--platform`, `--profile production`, `--path`, and `--wait` arguments; never
|
||||||
|
select `--latest`. If the result is ambiguous, inspect EAS and the store before
|
||||||
|
doing anything else, do not retry, and ask again before another upload.
|
||||||
|
|
||||||
|
Apply this gate to every upload method, including EAS, Fastlane, Xcode Organizer,
|
||||||
|
and the store consoles. A later public rollout, App Review submission, or
|
||||||
|
additional TestFlight distribution requires another fresh authorization.
|
||||||
|
|
||||||
|
Preserve platform signing and local EAS versioning. After a build, verify that
|
||||||
|
tracked changes are limited to the expected version metadata, then independently
|
||||||
|
read the artifact version and signature. Report the artifact, version/build
|
||||||
|
number, upload result when authorized, and store read-back state.
|
||||||
@@ -25,9 +25,15 @@ MCP service in one npm/Turborepo workspace.
|
|||||||
and prove the relevant compatibility contracts still hold. Preserve deployed
|
and prove the relevant compatibility contracts still hold. Preserve deployed
|
||||||
interfaces and supported client behavior; repository checks alone do not
|
interfaces and supported client behavior; repository checks alone do not
|
||||||
prove a live change is safe.
|
prove a live change is safe.
|
||||||
10. Scope work to the task. Route bounded implementation through the project
|
10. Delegate every substantive repository task to at least one project agent so
|
||||||
`implementation` agent when delegation is useful; the primary agent retains
|
detailed exploration or execution stays out of the primary context.
|
||||||
acceptance, review, and final verification. Do not delegate trivial edits.
|
"Substantive" includes repository exploration, multi-file reasoning,
|
||||||
|
implementation, test execution, or contract/risk analysis. Use `repo_scout`
|
||||||
|
for read-only investigation and `implementation` for bounded changes; use
|
||||||
|
the additional roles according to `docs/agents/routing.md`. The primary
|
||||||
|
agent retains scope, acceptance, review, and the final answer. Skip
|
||||||
|
delegation only for trivial conversation, a single obvious lookup, or a
|
||||||
|
mechanical one-line edit whose coordination cost exceeds the work.
|
||||||
11. Use high-effort reviewers only for the risk triggers in
|
11. Use high-effort reviewers only for the risk triggers in
|
||||||
`docs/agents/routing.md`.
|
`docs/agents/routing.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ export class PostgresRepository implements ApiRepository {
|
|||||||
target: profiles.id,
|
target: profiles.id,
|
||||||
set: {
|
set: {
|
||||||
email: sql`coalesce(excluded.email, ${profiles.email})`,
|
email: sql`coalesce(excluded.email, ${profiles.email})`,
|
||||||
authSyncedAt: new Date(),
|
// Keep the write on the same clock as the column default. API and
|
||||||
|
// PostgreSQL hosts can differ slightly, which must not move this
|
||||||
|
// monotonic sync marker backwards.
|
||||||
|
authSyncedAt: sql`now()`,
|
||||||
},
|
},
|
||||||
}).returning();
|
}).returning();
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ android {
|
|||||||
applicationId 'com.sauci.app'
|
applicationId 'com.sauci.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 47
|
versionCode 48
|
||||||
versionName "1.0.5"
|
versionName "1.0.6"
|
||||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Sauci",
|
"name": "Sauci",
|
||||||
"slug": "sauci",
|
"slug": "sauci",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"scheme": "app.sauci",
|
"scheme": "app.sauci",
|
||||||
@@ -16,12 +16,15 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": false,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.sauci.app",
|
"bundleIdentifier": "com.sauci.app",
|
||||||
|
"config": {
|
||||||
|
"usesNonExemptEncryption": false
|
||||||
|
},
|
||||||
"entitlements": {
|
"entitlements": {
|
||||||
"com.apple.security.application-groups": [
|
"com.apple.security.application-groups": [
|
||||||
"group.com.sauci.app"
|
"group.com.sauci.app"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"buildNumber": "43",
|
"buildNumber": "45",
|
||||||
"usesAppleSignIn": true,
|
"usesAppleSignIn": true,
|
||||||
"googleServicesFile": "./GoogleService-Info.plist",
|
"googleServicesFile": "./GoogleService-Info.plist",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
@@ -183,7 +186,7 @@
|
|||||||
"backgroundColor": "#1a1a2e"
|
"backgroundColor": "#1a1a2e"
|
||||||
},
|
},
|
||||||
"package": "com.sauci.app",
|
"package": "com.sauci.app",
|
||||||
"versionCode": 47,
|
"versionCode": 48,
|
||||||
"googleServicesFile": "./google-services.json",
|
"googleServicesFile": "./google-services.json",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"RECEIVE_BOOT_COMPLETED",
|
"RECEIVE_BOOT_COMPLETED",
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { ScrollView, StyleSheet, Platform, View, Text, useWindowDimensions } from 'react-native';
|
import { ScrollView, StyleSheet, Platform, View, Text, useWindowDimensions } from 'react-native';
|
||||||
import { useFocusEffect } from 'expo-router';
|
import { useFocusEffect, useRouter } from 'expo-router';
|
||||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
import { useAuthStore, usePacksStore, useSubscriptionStore } from '../../src/store';
|
import * as Haptics from 'expo-haptics';
|
||||||
|
import { useAuthStore, useMatchStore, usePacksStore, useSubscriptionStore } from '../../src/store';
|
||||||
import { GradientBackground } from '../../src/components/ui';
|
import { GradientBackground } from '../../src/components/ui';
|
||||||
import { Paywall } from '../../src/components/paywall';
|
import { Paywall } from '../../src/components/paywall';
|
||||||
import { CompactHeader, ContentRow, DaresTile, LiveDrawTile } from '../../src/components/discovery';
|
import { CompactHeader, ContentRow, DaresTile, LiveDrawTile, PendingQuestionsCard } from '../../src/components/discovery';
|
||||||
import { StreakDisplay } from '../../src/components/StreakDisplay';
|
import { StreakDisplay } from '../../src/components/StreakDisplay';
|
||||||
import { colors, spacing, typography, radius } from '../../src/theme';
|
import { Events } from '../../src/lib/analytics';
|
||||||
|
import { colors, spacing, typography } from '../../src/theme';
|
||||||
import type { QuestionPack, Category } from '../../src/types';
|
import type { QuestionPack, Category } from '../../src/types';
|
||||||
|
|
||||||
const MAX_CONTENT_WIDTH = 600;
|
const MAX_CONTENT_WIDTH = 600;
|
||||||
@@ -69,14 +71,21 @@ function groupPacksByCategory(
|
|||||||
export default function DiscoveryScreen() {
|
export default function DiscoveryScreen() {
|
||||||
const { user, partner, couple } = useAuthStore();
|
const { user, partner, couple } = useAuthStore();
|
||||||
const { packs, categories, fetchPacks, ensurePacksLoaded, getPackProgress } = usePacksStore();
|
const { packs, categories, fetchPacks, ensurePacksLoaded, getPackProgress } = usePacksStore();
|
||||||
|
const { pendingQuestions, fetchPendingQuestions } = useMatchStore();
|
||||||
const { subscription } = useSubscriptionStore();
|
const { subscription } = useSubscriptionStore();
|
||||||
|
const router = useRouter();
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
const [showPaywall, setShowPaywall] = useState(false);
|
const [showPaywall, setShowPaywall] = useState(false);
|
||||||
const [headerHeight, setHeaderHeight] = useState(130);
|
const [headerHeight, setHeaderHeight] = useState(130);
|
||||||
|
const lastPromptImpression = useRef<string | null>(null);
|
||||||
|
|
||||||
// Check all sources of premium access: user flag, partner flag, or active subscription
|
// Check all sources of premium access: user flag, partner flag, or active subscription
|
||||||
const isPremiumUser = user?.is_premium || partner?.is_premium || subscription.isProUser;
|
const isPremiumUser = user?.is_premium || partner?.is_premium || subscription.isProUser;
|
||||||
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
||||||
|
const pendingQuestionIds = useMemo(
|
||||||
|
() => pendingQuestions.map(question => question.id).join(','),
|
||||||
|
[pendingQuestions],
|
||||||
|
);
|
||||||
|
|
||||||
// Load the catalog once. Returning to this tab shows what is cached; answering a
|
// Load the catalog once. Returning to this tab shows what is cached; answering a
|
||||||
// question invalidates it, and StreakDisplay owns the streak, so neither needs a
|
// question invalidates it, and StreakDisplay owns the streak, so neither needs a
|
||||||
@@ -84,7 +93,17 @@ export default function DiscoveryScreen() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
void ensurePacksLoaded();
|
void ensurePacksLoaded();
|
||||||
}, [ensurePacksLoaded])
|
void fetchPendingQuestions();
|
||||||
|
|
||||||
|
if (pendingQuestionIds && lastPromptImpression.current !== pendingQuestionIds) {
|
||||||
|
Events.pendingQuestionsPromptViewed(pendingQuestions.length);
|
||||||
|
lastPromptImpression.current = pendingQuestionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
lastPromptImpression.current = null;
|
||||||
|
};
|
||||||
|
}, [ensurePacksLoaded, fetchPendingQuestions, pendingQuestionIds, pendingQuestions.length])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Group packs by category
|
// Group packs by category
|
||||||
@@ -96,6 +115,18 @@ export default function DiscoveryScreen() {
|
|||||||
// Check if user has no packs enabled (empty state)
|
// Check if user has no packs enabled (empty state)
|
||||||
const hasNoPacks = packs.length === 0;
|
const hasNoPacks = packs.length === 0;
|
||||||
|
|
||||||
|
const handlePendingQuestionsPress = useCallback(() => {
|
||||||
|
const firstQuestion = pendingQuestions[0];
|
||||||
|
if (!firstQuestion) return;
|
||||||
|
|
||||||
|
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
Events.pendingQuestionsPromptTapped(pendingQuestions.length);
|
||||||
|
router.push({
|
||||||
|
pathname: '/(app)/swipe',
|
||||||
|
params: { mode: 'pending', startQuestionId: firstQuestion.question.id },
|
||||||
|
});
|
||||||
|
}, [pendingQuestions, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientBackground>
|
<GradientBackground>
|
||||||
<View
|
<View
|
||||||
@@ -117,16 +148,23 @@ export default function DiscoveryScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={[styles.innerContainer, isWideScreen && styles.innerContainerWide]}>
|
<View style={[styles.innerContainer, isWideScreen && styles.innerContainerWide]}>
|
||||||
|
<PendingQuestionsCard
|
||||||
|
questions={pendingQuestions}
|
||||||
|
partnerName={partner?.name}
|
||||||
|
onPress={handlePendingQuestionsPress}
|
||||||
|
delay={50}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Shared streak - hidden entirely for couples with nothing running */}
|
{/* Shared streak - hidden entirely for couples with nothing running */}
|
||||||
<View style={styles.streakSlot}>
|
<View style={styles.streakSlot}>
|
||||||
<StreakDisplay delay={50} />
|
<StreakDisplay delay={100} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Live Draw Activity Tile */}
|
{/* Live Draw Activity Tile */}
|
||||||
<LiveDrawTile delay={100} />
|
<LiveDrawTile delay={150} />
|
||||||
|
|
||||||
{/* Dares */}
|
{/* Dares */}
|
||||||
<DaresTile delay={150} />
|
<DaresTile delay={200} />
|
||||||
|
|
||||||
{/* Content Rows by Category */}
|
{/* Content Rows by Category */}
|
||||||
{packsByCategory.map((group, index) => (
|
{packsByCategory.map((group, index) => (
|
||||||
@@ -136,7 +174,7 @@ export default function DiscoveryScreen() {
|
|||||||
packs={group.packs}
|
packs={group.packs}
|
||||||
isPremiumUser={isPremiumUser}
|
isPremiumUser={isPremiumUser}
|
||||||
categoryId={group.category.id}
|
categoryId={group.category.id}
|
||||||
delay={200 + index * 100}
|
delay={250 + index * 100}
|
||||||
getPackProgress={getPackProgress}
|
getPackProgress={getPackProgress}
|
||||||
onShowPaywall={() => setShowPaywall(true)}
|
onShowPaywall={() => setShowPaywall(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ module.exports = function (api) {
|
|||||||
api.cache(true);
|
api.cache(true);
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo'],
|
presets: ['babel-preset-expo'],
|
||||||
|
plugins: ['react-native-worklets/plugin'],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
+11
-1
@@ -32,6 +32,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
"production": {}
|
"production": {
|
||||||
|
"android": {
|
||||||
|
"track": "internal",
|
||||||
|
"releaseStatus": "draft",
|
||||||
|
"changesNotSentForReview": true
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"ascAppId": "6757159885",
|
||||||
|
"appleTeamId": "BXVPBATDZY"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2446
-1290
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"expo.jsEngine": "hermes",
|
"expo.jsEngine": "hermes",
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
"newArchEnabled": "false",
|
"newArchEnabled": "true",
|
||||||
"ios.useFrameworks": "static",
|
"ios.useFrameworks": "static",
|
||||||
"apple.extraPods": "[]",
|
"apple.extraPods": "[]",
|
||||||
"apple.ccacheEnabled": "false",
|
"apple.ccacheEnabled": "false",
|
||||||
|
|||||||
@@ -7,15 +7,14 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||||
5254BDADE70741A0B2504FB1 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = F744EAD4ABA54509966475D6 /* noop-file.swift */; };
|
5254BDADE70741A0B2504FB1 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = F744EAD4ABA54509966475D6 /* noop-file.swift */; };
|
||||||
7B71EEB2AF682FA7AFF556D5 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDDAD8C12C38D3BD0A76D345 /* Pods_Sauci.framework */; };
|
7B71EEB2AF682FA7AFF556D5 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDDAD8C12C38D3BD0A76D345 /* Pods_Sauci.framework */; };
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
CEF2F278ABBC5FBFD470A0F7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7B5C17768E3B74AC152531F /* PrivacyInfo.xcprivacy */; };
|
CEF2F278ABBC5FBFD470A0F7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7B5C17768E3B74AC152531F /* PrivacyInfo.xcprivacy */; };
|
||||||
|
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
||||||
FDFABE1A1A3547B1B22DD2DD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */; };
|
FDFABE1A1A3547B1B22DD2DD /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */; };
|
||||||
XX12B55A1EDABF083C3B51XX /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */; };
|
XX12B55A1EDABF083C3B51XX /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */; };
|
||||||
XX1A6800511A685AB346D2XX /* AppIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XXA12889081EB894481655XX /* AppIntents.framework */; };
|
XX1A6800511A685AB346D2XX /* AppIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XXA12889081EB894481655XX /* AppIntents.framework */; };
|
||||||
@@ -50,11 +49,8 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
13B07F961A680F5B00A75B9A /* Sauci.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; fileEncoding = 4; includeInIndex = 0; path = Sauci.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
13B07F961A680F5B00A75B9A /* Sauci.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; fileEncoding = 4; includeInIndex = 0; path = Sauci.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Sauci/AppDelegate.h; sourceTree = "<group>"; };
|
|
||||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Sauci/AppDelegate.mm; sourceTree = "<group>"; };
|
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Sauci/Images.xcassets; sourceTree = "<group>"; };
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sauci/Info.plist; sourceTree = "<group>"; };
|
||||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sauci/main.m; sourceTree = "<group>"; };
|
|
||||||
6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.debug.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.debug.xcconfig"; sourceTree = "<group>"; };
|
6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.debug.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.release.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.release.xcconfig"; sourceTree = "<group>"; };
|
7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.release.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Sauci/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Sauci/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||||
@@ -64,6 +60,7 @@
|
|||||||
DA536CAC88B349A59DFC1F1E /* Sauci-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Sauci-Bridging-Header.h"; path = "Sauci/Sauci-Bridging-Header.h"; sourceTree = "<group>"; };
|
DA536CAC88B349A59DFC1F1E /* Sauci-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Sauci-Bridging-Header.h"; path = "Sauci/Sauci-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
DDDAD8C12C38D3BD0A76D345 /* Pods_Sauci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; fileEncoding = 4; includeInIndex = undefined; path = Pods_Sauci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
DDDAD8C12C38D3BD0A76D345 /* Pods_Sauci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; fileEncoding = 4; includeInIndex = undefined; path = Pods_Sauci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
|
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Sauci/AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
F744EAD4ABA54509966475D6 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Sauci/noop-file.swift"; sourceTree = "<group>"; };
|
F744EAD4ABA54509966475D6 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Sauci/noop-file.swift"; sourceTree = "<group>"; };
|
||||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||||
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
@@ -126,11 +123,9 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
||||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
|
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||||
13B07FB71A68108700A75B9A /* main.m */,
|
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||||
A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */,
|
A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */,
|
||||||
F744EAD4ABA54509966475D6 /* noop-file.swift */,
|
F744EAD4ABA54509966475D6 /* noop-file.swift */,
|
||||||
@@ -421,6 +416,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.bundle",
|
||||||
@@ -455,6 +451,7 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.bundle",
|
||||||
@@ -497,11 +494,16 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"$(SRCROOT)/.xcode.env",
|
||||||
|
"$(SRCROOT)/.xcode.env.local",
|
||||||
|
"$(SRCROOT)/Sauci/Sauci.entitlements",
|
||||||
|
"$(SRCROOT)/Pods/Target Support Files/Pods-Sauci/expo-configure-project.sh",
|
||||||
);
|
);
|
||||||
name = "[Expo] Configure project";
|
name = "[Expo] Configure project";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
"$(SRCROOT)/Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
@@ -527,8 +529,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||||
5254BDADE70741A0B2504FB1 /* noop-file.swift in Sources */,
|
5254BDADE70741A0B2504FB1 /* noop-file.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -569,7 +570,7 @@
|
|||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0.5;
|
MARKETING_VERSION = 1.0.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -598,7 +599,7 @@
|
|||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0.5;
|
MARKETING_VERSION = 1.0.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -671,6 +672,8 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios",
|
||||||
);
|
);
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||||
@@ -738,6 +741,8 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios",
|
||||||
);
|
);
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||||
@@ -767,7 +772,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 41;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@@ -777,7 +782,7 @@
|
|||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
MARKETING_VERSION = 1.0.5;
|
MARKETING_VERSION = 1.0.6;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app.LiveDrawWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app.LiveDrawWidget;
|
||||||
@@ -803,7 +808,7 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 41;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@@ -813,7 +818,7 @@
|
|||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
MARKETING_VERSION = 1.0.5;
|
MARKETING_VERSION = 1.0.6;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#import <RCTAppDelegate.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import <Expo/Expo.h>
|
|
||||||
|
|
||||||
@interface AppDelegate : EXAppDelegateWrapper
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#import "AppDelegate.h"
|
|
||||||
#import <Firebase/Firebase.h>
|
|
||||||
|
|
||||||
#import <React/RCTBundleURLProvider.h>
|
|
||||||
#import <React/RCTLinkingManager.h>
|
|
||||||
|
|
||||||
@implementation AppDelegate
|
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
|
||||||
{
|
|
||||||
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
|
|
||||||
[FIRApp configure];
|
|
||||||
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
|
|
||||||
self.moduleName = @"main";
|
|
||||||
|
|
||||||
// You can add your custom initial props in the dictionary below.
|
|
||||||
// They will be passed down to the ViewController used by React Native.
|
|
||||||
self.initialProps = @{};
|
|
||||||
|
|
||||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
|
||||||
{
|
|
||||||
return [self bundleURL];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSURL *)bundleURL
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
|
|
||||||
#else
|
|
||||||
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// Linking API
|
|
||||||
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
|
|
||||||
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Universal Links
|
|
||||||
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
|
|
||||||
BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
|
|
||||||
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
|
||||||
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
|
|
||||||
{
|
|
||||||
return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
|
||||||
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
|
|
||||||
{
|
|
||||||
return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
|
|
||||||
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
|
|
||||||
{
|
|
||||||
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import Expo
|
||||||
|
import FirebaseCore
|
||||||
|
import React
|
||||||
|
import ReactAppDependencyProvider
|
||||||
|
|
||||||
|
@UIApplicationMain
|
||||||
|
public class AppDelegate: ExpoAppDelegate {
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
|
||||||
|
var reactNativeFactory: RCTReactNativeFactory?
|
||||||
|
|
||||||
|
public override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||||
|
) -> Bool {
|
||||||
|
let delegate = ReactNativeDelegate()
|
||||||
|
let factory = ExpoReactNativeFactory(delegate: delegate)
|
||||||
|
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||||
|
|
||||||
|
reactNativeDelegate = delegate
|
||||||
|
reactNativeFactory = factory
|
||||||
|
bindReactNativeFactory(factory)
|
||||||
|
|
||||||
|
#if os(iOS) || os(tvOS)
|
||||||
|
window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
|
FirebaseApp.configure()
|
||||||
|
factory.startReactNative(
|
||||||
|
withModuleName: "main",
|
||||||
|
in: window,
|
||||||
|
launchOptions: launchOptions)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func application(
|
||||||
|
_ app: UIApplication,
|
||||||
|
open url: URL,
|
||||||
|
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||||
|
) -> Bool {
|
||||||
|
super.application(app, open: url, options: options)
|
||||||
|
|| RCTLinkingManager.application(app, open: url, options: options)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
continue userActivity: NSUserActivity,
|
||||||
|
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
||||||
|
) -> Bool {
|
||||||
|
let result = RCTLinkingManager.application(
|
||||||
|
application,
|
||||||
|
continue: userActivity,
|
||||||
|
restorationHandler: restorationHandler)
|
||||||
|
return super.application(
|
||||||
|
application,
|
||||||
|
continue: userActivity,
|
||||||
|
restorationHandler: restorationHandler) || result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
|
||||||
|
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||||||
|
bridge.bundleURL ?? bundleURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func bundleURL() -> URL? {
|
||||||
|
#if DEBUG
|
||||||
|
RCTBundleURLProvider.sharedSettings()
|
||||||
|
.jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
||||||
|
#else
|
||||||
|
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
-106
@@ -1,109 +1,113 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleAllowMixedLocalizations</key>
|
<key>CFBundleAllowMixedLocalizations</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Sauci</string>
|
<string>Sauci</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.5</string>
|
<string>1.0.6</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app.sauci</string>
|
<string>app.sauci</string>
|
||||||
<string>com.sauci.app</string>
|
<string>com.sauci.app</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>exp+sauci</string>
|
<string>exp+sauci</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>com.googleusercontent.apps.861972684340-0u2as8i5equfhmhmvjihottkf0ig7237</string>
|
<string>com.googleusercontent.apps.861972684340-0u2as8i5equfhmhmvjihottkf0ig7237</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>43</string>
|
<string>45</string>
|
||||||
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<string>12.0</string>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<true/>
|
<string>12.0</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<dict>
|
<true/>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<false/>
|
<dict>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<false/>
|
||||||
</dict>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
<key>NSCameraUsageDescription</key>
|
<true/>
|
||||||
<string>Sauci uses your camera to take photos and videos that you can share privately with your partner.</string>
|
</dict>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Sauci uses Face ID to unlock the app and keep your conversations private.</string>
|
<string>Sauci uses your camera to take photos and videos that you can share privately with your partner.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Sauci uses your microphone to record audio when you capture videos to share privately with your partner.</string>
|
<string>Sauci uses Face ID to unlock the app and keep your conversations private.</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Sauci saves photos and videos from your partner's messages to your photo library when you tap the save button.</string>
|
<string>Sauci uses your microphone to record audio when you capture videos to share privately with your partner.</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>Sauci uses your photo library so you can select photos and videos to share privately with your partner.</string>
|
<string>Sauci saves photos and videos from your partner's messages to your photo library when you tap the save button.</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<array>
|
<string>Sauci uses your photo library so you can select photos and videos to share privately with your partner.</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<key>NSUserActivityTypes</key>
|
||||||
</array>
|
<array>
|
||||||
<key>UIBackgroundModes</key>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
<array>
|
</array>
|
||||||
<string>remote-notification</string>
|
<key>RCTNewArchEnabled</key>
|
||||||
</array>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UIBackgroundModes</key>
|
||||||
<string>SplashScreen</string>
|
<array>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<string>remote-notification</string>
|
||||||
<array>
|
</array>
|
||||||
<string>arm64</string>
|
<key>UILaunchStoryboardName</key>
|
||||||
</array>
|
<string>SplashScreen</string>
|
||||||
<key>UIRequiresFullScreen</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
<false/>
|
<array>
|
||||||
<key>UIStatusBarStyle</key>
|
<string>arm64</string>
|
||||||
<string>UIStatusBarStyleDefault</string>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UIRequiresFullScreen</key>
|
||||||
<array>
|
<false/>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UIStatusBarStyle</key>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIStatusBarStyleDefault</string>
|
||||||
</array>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<array>
|
||||||
<array>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
</array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<array>
|
||||||
</array>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<key>UIUserInterfaceStyle</key>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>Automatic</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<false/>
|
</array>
|
||||||
</dict>
|
<key>UIUserInterfaceStyle</key>
|
||||||
</plist>
|
<string>Automatic</string>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
#import "AppDelegate.h"
|
|
||||||
|
|
||||||
int main(int argc, char * argv[]) {
|
|
||||||
@autoreleasepool {
|
|
||||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { fireEvent, render } from '@/test/test-utils';
|
||||||
|
import { PendingQuestionsCard, formatWaitingSince } from '@/components/discovery/PendingQuestionsCard';
|
||||||
|
import type { PendingQuestion } from '@/store';
|
||||||
|
|
||||||
|
const pendingQuestion = (id: string, partnerAnsweredAt: string): PendingQuestion => ({
|
||||||
|
id: `response-${id}`,
|
||||||
|
partnerAnsweredAt,
|
||||||
|
question: {
|
||||||
|
id,
|
||||||
|
pack_id: 'pack-1',
|
||||||
|
text: `Question ${id}`,
|
||||||
|
intensity: 1,
|
||||||
|
created_at: '2026-08-01T00:00:00.000Z',
|
||||||
|
pack: { id: 'pack-1', name: 'Connection', icon: 'heart' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PendingQuestionsCard', () => {
|
||||||
|
it('stays out of Home when there is nothing to answer', () => {
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<PendingQuestionsCard questions={[]} partnerName="Sam" onPress={jest.fn()} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByTestId('pending-questions-card')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('names the partner, shows the count, and preserves answer privacy', () => {
|
||||||
|
const questions = [
|
||||||
|
pendingQuestion('q1', '2026-08-29T09:00:00.000Z'),
|
||||||
|
pendingQuestion('q2', '2026-08-29T10:00:00.000Z'),
|
||||||
|
];
|
||||||
|
const { getByText, getByLabelText } = render(
|
||||||
|
<PendingQuestionsCard questions={questions} partnerName="Sam" onPress={jest.fn()} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Sam has answered 2 questions')).toBeTruthy();
|
||||||
|
expect(getByText('Answer yours to see where you match. Their answers stay private until you respond.')).toBeTruthy();
|
||||||
|
expect(getByLabelText('2 questions waiting')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts the answer action from its accessible CTA', () => {
|
||||||
|
const onPress = jest.fn();
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<PendingQuestionsCard
|
||||||
|
questions={[pendingQuestion('q1', '2026-08-29T09:00:00.000Z')]}
|
||||||
|
partnerName="Sam"
|
||||||
|
onPress={onPress}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.press(getByTestId('pending-questions-answer'));
|
||||||
|
expect(onPress).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatWaitingSince', () => {
|
||||||
|
const now = new Date('2026-08-29T12:00:00.000Z');
|
||||||
|
|
||||||
|
it('formats useful compact waiting ages', () => {
|
||||||
|
expect(formatWaitingSince('2026-08-29T11:59:30.000Z', now)).toBe('just now');
|
||||||
|
expect(formatWaitingSince('2026-08-29T11:42:00.000Z', now)).toBe('18m ago');
|
||||||
|
expect(formatWaitingSince('2026-08-29T09:00:00.000Z', now)).toBe('3h ago');
|
||||||
|
expect(formatWaitingSince('2026-08-27T12:00:00.000Z', now)).toBe('2d ago');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import { handleNotificationResponse } from '@/lib/notifications';
|
||||||
|
import { useMatchStore } from '@/store/matchStore';
|
||||||
|
|
||||||
|
jest.mock('expo-router', () => ({
|
||||||
|
router: { push: jest.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('expo-notifications', () => ({
|
||||||
|
setNotificationHandler: jest.fn(),
|
||||||
|
addNotificationResponseReceivedListener: jest.fn(),
|
||||||
|
getLastNotificationResponseAsync: jest.fn(),
|
||||||
|
getPermissionsAsync: jest.fn(),
|
||||||
|
requestPermissionsAsync: jest.fn(),
|
||||||
|
getExpoPushTokenAsync: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/lib/crashlytics', () => ({
|
||||||
|
captureError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const notificationResponse = (type: string) => ({
|
||||||
|
notification: {
|
||||||
|
request: {
|
||||||
|
content: { data: { type } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) as never;
|
||||||
|
|
||||||
|
describe('handleNotificationResponse', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useMatchStore.setState({ currentView: 'active' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the answer queue for a catch-up reminder', () => {
|
||||||
|
handleNotificationResponse(notificationResponse('catchup_reminder'));
|
||||||
|
|
||||||
|
expect(useMatchStore.getState().currentView).toBe('pending');
|
||||||
|
expect(router.push).toHaveBeenCalledWith('/(app)/swipe?mode=pending');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores unknown notification types', () => {
|
||||||
|
handleNotificationResponse(notificationResponse('unknown'));
|
||||||
|
|
||||||
|
expect(router.push).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||||
|
import type { PendingQuestion } from '../../store';
|
||||||
|
import { colors, gradients, radius, shadows, spacing, typography } from '../../theme';
|
||||||
|
|
||||||
|
interface PendingQuestionsCardProps {
|
||||||
|
questions: PendingQuestion[];
|
||||||
|
partnerName?: string | null;
|
||||||
|
onPress: () => void;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWaitingSince(dateString: string, now = new Date()): string {
|
||||||
|
const answeredAt = new Date(dateString);
|
||||||
|
const diffMs = Math.max(0, now.getTime() - answeredAt.getTime());
|
||||||
|
const minutes = Math.floor(diffMs / 60_000);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (minutes < 1) return 'just now';
|
||||||
|
if (minutes < 60) return `${minutes}m ago`;
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
if (days < 7) return `${days}d ago`;
|
||||||
|
return answeredAt.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PendingQuestionsCard({
|
||||||
|
questions,
|
||||||
|
partnerName,
|
||||||
|
onPress,
|
||||||
|
delay = 0,
|
||||||
|
}: PendingQuestionsCardProps) {
|
||||||
|
const oldestQuestion = useMemo(
|
||||||
|
() => questions.reduce<PendingQuestion | null>((oldest, question) => {
|
||||||
|
if (!oldest) return question;
|
||||||
|
return new Date(question.partnerAnsweredAt) < new Date(oldest.partnerAnsweredAt)
|
||||||
|
? question
|
||||||
|
: oldest;
|
||||||
|
}, null),
|
||||||
|
[questions],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!oldestQuestion) return null;
|
||||||
|
|
||||||
|
const count = questions.length;
|
||||||
|
const subject = partnerName?.trim() || 'Your partner';
|
||||||
|
const questionLabel = count === 1 ? 'question' : 'questions';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.delay(delay).duration(450).springify()}
|
||||||
|
style={styles.container}
|
||||||
|
testID="pending-questions-card"
|
||||||
|
>
|
||||||
|
<View style={[styles.card, shadows.md]}>
|
||||||
|
<View style={styles.accent} />
|
||||||
|
|
||||||
|
<View style={styles.labelRow}>
|
||||||
|
<View style={styles.labelGroup}>
|
||||||
|
<View style={styles.statusDot} />
|
||||||
|
<Text style={styles.label}>Your turn</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.countBadge} accessibilityLabel={`${count} ${questionLabel} waiting`}>
|
||||||
|
<Text style={styles.countText}>{count}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.title}>
|
||||||
|
{subject} has answered {count} {questionLabel}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.description}>
|
||||||
|
Answer yours to see where you match. Their answers stay private until you respond.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Answer ${count} waiting ${questionLabel}`}
|
||||||
|
testID="pending-questions-answer"
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={gradients.primary as [string, string, ...string[]]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Answer now</Text>
|
||||||
|
<Ionicons name="arrow-forward" size={17} color={colors.text} />
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.waitingRow}>
|
||||||
|
<Ionicons name="time-outline" size={13} color={colors.textTertiary} />
|
||||||
|
<Text style={styles.waitingText}>
|
||||||
|
Oldest waiting · {formatWaitingSince(oldestQuestion.partnerAnsweredAt)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: spacing.md,
|
||||||
|
borderRadius: radius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(232, 164, 174, 0.36)',
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
borderBottomLeftRadius: 96,
|
||||||
|
backgroundColor: colors.primaryLight,
|
||||||
|
},
|
||||||
|
labelRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
labelGroup: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
statusDot: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
shadowColor: colors.primary,
|
||||||
|
shadowOpacity: 0.55,
|
||||||
|
shadowRadius: 6,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
...typography.caption1,
|
||||||
|
color: colors.premium.rose,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
countBadge: {
|
||||||
|
minWidth: 32,
|
||||||
|
height: 26,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
borderRadius: radius.full,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
},
|
||||||
|
countText: {
|
||||||
|
...typography.caption1,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: '800',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
...typography.title3,
|
||||||
|
color: colors.text,
|
||||||
|
marginTop: spacing.md,
|
||||||
|
paddingRight: spacing.xl,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
...typography.subhead,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
minHeight: 46,
|
||||||
|
borderRadius: radius.md,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
...typography.callout,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
waitingRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.xs,
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
},
|
||||||
|
waitingText: {
|
||||||
|
...typography.caption2,
|
||||||
|
color: colors.textTertiary,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,3 +4,4 @@ export { CompactHeader } from './CompactHeader';
|
|||||||
export { CircularProgress } from './CircularProgress';
|
export { CircularProgress } from './CircularProgress';
|
||||||
export { LiveDrawTile } from './LiveDrawTile';
|
export { LiveDrawTile } from './LiveDrawTile';
|
||||||
export { DaresTile } from './DaresTile';
|
export { DaresTile } from './DaresTile';
|
||||||
|
export { PendingQuestionsCard } from './PendingQuestionsCard';
|
||||||
@@ -216,6 +216,10 @@ export const Events = {
|
|||||||
questionSkipped: () => logEvent("question_skipped"),
|
questionSkipped: () => logEvent("question_skipped"),
|
||||||
matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
|
matchCreated: (matchType: string) => logEvent("match_created", { match_type: matchType }),
|
||||||
allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
|
allQuestionsExhausted: () => logEvent("all_questions_exhausted"),
|
||||||
|
pendingQuestionsPromptViewed: (count: number) =>
|
||||||
|
logEvent("pending_questions_prompt_viewed", { question_count: count }),
|
||||||
|
pendingQuestionsPromptTapped: (count: number) =>
|
||||||
|
logEvent("pending_questions_prompt_tapped", { question_count: count }),
|
||||||
|
|
||||||
// Chat events
|
// Chat events
|
||||||
matchViewed: () => logEvent("match_viewed"),
|
matchViewed: () => logEvent("match_viewed"),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Notifications.setNotificationHandler({
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
export interface NotificationData {
|
export interface NotificationData {
|
||||||
type: "match" | "message" | "match_digest" | "partner_activity" | "nudge" | "weekly_summary";
|
type: "match" | "message" | "match_digest" | "partner_activity" | "nudge" | "weekly_summary" | "catchup_reminder";
|
||||||
match_id?: string;
|
match_id?: string;
|
||||||
message_id?: string;
|
message_id?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
@@ -36,6 +36,7 @@ const notificationTypes = new Set<NotificationData["type"]>([
|
|||||||
"partner_activity",
|
"partner_activity",
|
||||||
"nudge",
|
"nudge",
|
||||||
"weekly_summary",
|
"weekly_summary",
|
||||||
|
"catchup_reminder",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isNotificationData(value: unknown): value is NotificationData {
|
function isNotificationData(value: unknown): value is NotificationData {
|
||||||
@@ -254,12 +255,18 @@ export function handleNotificationResponse(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === "partner_activity") {
|
if (data.type === "partner_activity") {
|
||||||
// Navigate to Matches > Your Turn tab (questions to answer)
|
// Navigate to Matches > Your Turn tab (questions to answer)
|
||||||
useMatchStore.getState().setCurrentView('pending');
|
useMatchStore.getState().setCurrentView('pending');
|
||||||
router.push("/(app)/matches");
|
router.push("/(app)/matches");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.type === "catchup_reminder") {
|
||||||
|
useMatchStore.getState().setCurrentView('pending');
|
||||||
|
router.push("/(app)/swipe?mode=pending");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.match_id) {
|
if (!data.match_id) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<string>com.apple.widgetkit-extension</string>
|
<string>com.apple.widgetkit-extension</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>43</string>
|
<string>45</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.5</string>
|
<string>1.0.6</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
# Agent routing and effort
|
# Agent routing and effort
|
||||||
|
|
||||||
The primary agent owns scope, acceptance criteria, task decomposition, and the
|
The primary agent owns scope, acceptance criteria, task decomposition, review,
|
||||||
final answer. Use one implementation agent when a bounded handoff will keep noisy
|
and the final answer. Every substantive repository task must delegate at least
|
||||||
execution out of the primary context. Do not pay the coordination cost for a
|
one bounded unit to the best-fit project agent. Use `repo_scout` for read-only
|
||||||
trivial one-file edit. Add other agents only for separable work or a risk trigger.
|
discovery or analysis and `implementation` for product or harness changes. Add
|
||||||
|
other agents only for separable work or the risk triggers below. Skip delegation
|
||||||
|
only for trivial conversation, a single obvious lookup, or a mechanical one-line
|
||||||
|
edit whose coordination cost exceeds the work.
|
||||||
|
|
||||||
## Implementation and integration
|
## Implementation and integration
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# Sauci Mobile Release Notes
|
# Sauci Mobile Release Notes
|
||||||
|
|
||||||
|
## v1.0.6 — 30 August 2026
|
||||||
|
_iOS build 44 · Android build 48_
|
||||||
|
|
||||||
|
A little more clarity, momentum, and playfulness for your shared check-ins.
|
||||||
|
|
||||||
|
- **Your turn at a glance** — A private Home prompt now shows when questions from your partner are waiting, without revealing their answers.
|
||||||
|
- **New ways to play** — Browse, send, and respond to Dares together from a dedicated shared space.
|
||||||
|
- **Streaks that fit your day** — Shared streaks now follow your local day, with clearer progress and reminder controls.
|
||||||
|
- **Smoother shared moments** — Leaner syncing keeps questions, matches, chat, and Live Draw feeling current with less unnecessary refreshing.
|
||||||
|
|
||||||
## v1.0.1 (Build 30) - January 2025
|
## v1.0.1 (Build 30) - January 2025
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|||||||
+68
-15
@@ -33,6 +33,46 @@ Output files are placed in the current directory by default.
|
|||||||
These scripts create signed artifacts only. They do not upload or submit them.
|
These scripts create signed artifacts only. They do not upload or submit them.
|
||||||
Store mutation always requires a separate, explicitly authorized command.
|
Store mutation always requires a separate, explicitly authorized command.
|
||||||
|
|
||||||
|
## Submit a local artifact with EAS
|
||||||
|
|
||||||
|
The production submission credentials are managed by EAS. Never add App Store
|
||||||
|
Connect `.p8` files, Google service-account JSON files, or local credential paths
|
||||||
|
to the repository or `eas.json`.
|
||||||
|
|
||||||
|
Submit only an independently verified artifact by its exact absolute path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mobile
|
||||||
|
|
||||||
|
# Uploads an Android internal-track draft; it does not roll out the release.
|
||||||
|
eas submit --platform android --profile production --path /absolute/path/to/sauci.aab --wait
|
||||||
|
|
||||||
|
# Uploads to App Store Connect/TestFlight; it does not submit for App Review.
|
||||||
|
eas submit --platform ios --profile production --path /absolute/path/to/sauci.ipa --wait
|
||||||
|
```
|
||||||
|
|
||||||
|
Agents must not upload immediately after building, whether they use EAS,
|
||||||
|
Fastlane, Xcode Organizer, or a store console. Once the artifact is verified, the
|
||||||
|
agent must show the user the platform, exact path, bundle/package identifier,
|
||||||
|
version, build number, signing identity, SHA-256 digest, and destination, then ask
|
||||||
|
for a fresh confirmation to upload it. This confirmation authorizes one upload
|
||||||
|
of that exact artifact to that exact destination, even if the earlier request
|
||||||
|
already said to build, release, upload, or submit.
|
||||||
|
|
||||||
|
Before an iOS confirmation, inspect App Store Connect for automatic TestFlight
|
||||||
|
distribution. If upload will add the build to existing groups or testers, name
|
||||||
|
them in the confirmation and include that effect in the authorization. If the
|
||||||
|
effect cannot be established, stop. Changing automatic-distribution settings is
|
||||||
|
a separate action and requires its own authorization.
|
||||||
|
|
||||||
|
Do not use `--latest` for a local release. Before upload, verify the artifact's
|
||||||
|
package or bundle identifier, public version, build number, signature, and
|
||||||
|
SHA-256 digest. After upload, read back the same identity and version from Google
|
||||||
|
Play Console or App Store Connect. Public rollout, App Review submission, and
|
||||||
|
additional TestFlight group distribution remain separate, explicitly authorized
|
||||||
|
actions. If an upload command returns an ambiguous result, inspect EAS and the
|
||||||
|
store first; do not retry unless the user gives fresh authorization.
|
||||||
|
|
||||||
### Source integrity
|
### Source integrity
|
||||||
|
|
||||||
Sauci commits its native `ios/` and `android/` projects. Start a release from a
|
Sauci commits its native `ios/` and `android/` projects. Start a release from a
|
||||||
@@ -194,14 +234,21 @@ The AAB file will be at:
|
|||||||
android/app/build/outputs/bundle/release/app-release.aab
|
android/app/build/outputs/bundle/release/app-release.aab
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Upload to Play Store
|
### Step 5: Upload to Play Store
|
||||||
|
|
||||||
1. Go to [Google Play Console](https://play.google.com/console)
|
Apply the upload authorization gate above before opening or changing the release.
|
||||||
2. Select **Sauci** app
|
Uploading the AAB and starting any rollout are separate actions; ask again before
|
||||||
3. Go to **Production** (or testing track)
|
the rollout.
|
||||||
4. **Create new release**
|
|
||||||
5. Upload the `.aab` file
|
1. Go to [Google Play Console](https://play.google.com/console)
|
||||||
6. Add release notes and submit
|
2. Select **Sauci** app
|
||||||
|
3. Go to **Internal testing**, unless the user authorized a different exact track
|
||||||
|
4. **Create new release**
|
||||||
|
5. Upload the `.aab` file
|
||||||
|
6. Add release notes, save the release as a draft, and read it back
|
||||||
|
|
||||||
|
Do not send the draft for review or start a rollout without the separate fresh
|
||||||
|
authorization described above.
|
||||||
|
|
||||||
### Verifying Keystore Fingerprint
|
### Verifying Keystore Fingerprint
|
||||||
|
|
||||||
@@ -211,7 +258,7 @@ To verify you're using the correct keystore:
|
|||||||
keytool -list -v -keystore android/sauci-release.keystore -storepass <password>
|
keytool -list -v -keystore android/sauci-release.keystore -storepass <password>
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected SHA1: `C3:70:C5:8C:85:E5:B5:92:97:82:A9:B0:47:98:40:99:AC:C8:47:43`
|
Expected EAS production upload-key SHA1: `F0:60:26:C7:54:B0:46:F2:80:FE:AC:43:8A:69:10:BD:9A:62:B4:59`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -237,16 +284,22 @@ cd ..
|
|||||||
3. Go to **Product** → **Archive**
|
3. Go to **Product** → **Archive**
|
||||||
4. Wait for the build to complete
|
4. Wait for the build to complete
|
||||||
|
|
||||||
### Step 4: Upload to App Store
|
### Step 4: Upload to App Store
|
||||||
|
|
||||||
1. In Xcode Organizer (Window → Organizer), select the archive
|
Apply the upload authorization gate above before using Xcode Organizer. Include
|
||||||
|
any verified automatic TestFlight distribution in the confirmation.
|
||||||
|
|
||||||
|
1. In Xcode Organizer (Window → Organizer), select the archive
|
||||||
2. Click **Distribute App**
|
2. Click **Distribute App**
|
||||||
3. Select **App Store Connect** → **Upload**
|
3. Select **App Store Connect** → **Upload**
|
||||||
4. Follow the prompts to upload
|
4. Follow the prompts to upload
|
||||||
|
|
||||||
### Step 5: Submit in App Store Connect
|
### Step 5: Submit in App Store Connect
|
||||||
|
|
||||||
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
|
Submitting for App Review is not authorized by the upload confirmation. Ask the
|
||||||
|
user for fresh authorization immediately before this step.
|
||||||
|
|
||||||
|
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
|
||||||
2. Select **Sauci**
|
2. Select **Sauci**
|
||||||
3. Create a new version or select the uploaded build
|
3. Create a new version or select the uploaded build
|
||||||
4. Fill in release notes and submit for review
|
4. Fill in release notes and submit for review
|
||||||
|
|||||||
Reference in new issue
Block a user