Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4687f95fe | ||
|
|
53a48ab6d5 | ||
|
|
a6233d0175 | ||
|
|
c776859183 | ||
|
|
cc12f5f0ff | ||
|
|
3659cc8c2f | ||
|
|
4b227ad62c | ||
|
|
7c8d91c89c | ||
|
|
fe1248c59d | ||
|
|
6883befff3 | ||
|
|
a06f1b69e8 | ||
|
|
3a019e62d3 | ||
|
|
636811e66a | ||
|
|
84712a950e | ||
|
|
d14869d745 | ||
|
|
f1573f4906 | ||
|
|
d825e3d80c | ||
|
|
13094267a6 | ||
|
|
ecc31a13a2 | ||
|
|
7a76936829 | ||
|
|
83df3b45f8 | ||
|
|
9609a9084b | ||
|
|
90cde4aca4 | ||
|
|
ecff9e2ec8 | ||
|
|
ffc655f576 | ||
|
|
d8bedead0c | ||
|
|
6c3a181613 | ||
|
|
d8ff39a2f3 | ||
|
|
b8cb2addff | ||
|
|
5e28117662 | ||
|
|
196428f796 | ||
|
|
6bd04a6c06 | ||
|
|
7aca39aa8f | ||
|
|
79233f58aa | ||
|
|
c9e8835810 | ||
|
|
ff8f99f418 | ||
|
|
604eec3322 | ||
|
|
dbf548c7bd | ||
|
|
4973f2c631 | ||
|
|
2b05e50312 | ||
|
|
26312e5a58 | ||
|
|
66c25849e9 | ||
|
|
5c4a4608b1 | ||
|
|
659cee76b6 | ||
|
|
6bc4e31719 | ||
|
|
aa062c9fd0 | ||
|
|
5947220fc6 | ||
|
|
8bf93f372b |
No files matched your search
@@ -3,8 +3,8 @@ id: 2026-08-30-ios-extension-version-verification
|
|||||||
date: 2026-08-30
|
date: 2026-08-30
|
||||||
status: promoted
|
status: promoted
|
||||||
scope: iOS release artifact verification
|
scope: iOS release artifact verification
|
||||||
evidence: build 45 Xcode archive warning plus direct PlistBuddy inspection of the exported IPA
|
evidence: v1.0.6 exported IPA inspection and reproduced v1.0.7 Xcode build-setting mismatch
|
||||||
destination: .codex/skills/release-mobile/SKILL.md
|
destination: apps/mobile/scripts/release-preflight.mjs and .codex/skills/release-mobile/SKILL.md
|
||||||
review_after: 2027-02-28
|
review_after: 2027-02-28
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -21,19 +21,23 @@ and every packaged extension `Info.plist` with `PlistBuddy`.
|
|||||||
|
|
||||||
## Cause
|
## Cause
|
||||||
|
|
||||||
The archive warning described an intermediate state and was not authoritative
|
The v1.0.6 archive warning described an intermediate state and was not
|
||||||
for the exported package. Direct IPA inspection showed both the app and
|
authoritative for that exported package. The later v1.0.7 reproduction found a
|
||||||
`LiveDrawWidget.appex` at version 1.0.6 build 45.
|
separate real hazard: `CURRENT_PROJECT_VERSION` in both LiveDrawWidget
|
||||||
|
configurations can override the widget plist and package an older build number.
|
||||||
|
|
||||||
## Remediation
|
## Remediation
|
||||||
|
|
||||||
For every iOS release, inspect version and build values in the exported app and
|
Before every iOS release, require public-version parity across app config, both
|
||||||
all packaged extensions. Reject or rebuild only when the final IPA values differ;
|
plists, and Xcode marketing settings. Require the widget Debug and Release
|
||||||
do not accept or reject a release from archive warnings alone.
|
`CURRENT_PROJECT_VERSION` values to equal both plists. Then inspect the exported
|
||||||
|
app and all packaged extensions. Reject or rebuild only when the final IPA values
|
||||||
|
differ; do not accept or reject a release from archive warnings alone.
|
||||||
|
|
||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
`PlistBuddy` read `CFBundleShortVersionString` 1.0.6 and `CFBundleVersion` 45 from
|
`PlistBuddy` read `CFBundleShortVersionString` 1.0.6 and `CFBundleVersion` 45 from
|
||||||
both packaged plists. `codesign --verify --deep --strict` passed for the app, and
|
both v1.0.6 packaged plists. The v1.0.7 build later proved a stale widget Xcode
|
||||||
the verified IPA SHA-256 was
|
setting can survive plist-only review. `release-preflight.mjs` and its node test
|
||||||
`9d4779bddac4647911a3a38e4ee37cf794c2b8691e0746a041b629c62b308e7f`.
|
now reject that mismatch before EAS allocation; exported IPA inspection remains
|
||||||
|
the final authority.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
id: 2026-08-31-expo-prebuild-native-regression
|
||||||
|
date: 2026-08-31
|
||||||
|
status: promoted
|
||||||
|
scope: apps/mobile native projects (ios/, android/) and expo prebuild
|
||||||
|
evidence: v1.0.8 SDK 54 to 56 upgrade, commits 604eec3, 7aca39a, 6bd04a6, 79233f5, b8cb2add
|
||||||
|
destination: apps/mobile/scripts/release-preflight.mjs, apps/mobile/scripts/release-preflight.test.mjs, and docs/releasing.md
|
||||||
|
review_after: 2026-11-30
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
`expo prebuild --clean` during the SDK 54 to 56 upgrade (and the
|
||||||
|
@bacons/apple-targets 0.1 to 5.0 bump) silently regressed four checked-in
|
||||||
|
native invariants. Each regression was caught only by a release gate or by
|
||||||
|
manual diffing against the last shipped tag (3fc6f8a, v1.0.7), not by
|
||||||
|
prebuild itself.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
Run `expo prebuild --clean` in apps/mobile against the upgraded SDK and diff
|
||||||
|
the regenerated ios/ and android/ projects against the previously committed
|
||||||
|
projects.
|
||||||
|
|
||||||
|
## Cause
|
||||||
|
|
||||||
|
1. Widget identity: apple-targets 5.x derives PRODUCT_BUNDLE_IDENTIFIER from
|
||||||
|
the target folder name, changing com.sauci.app.LiveDrawWidget to
|
||||||
|
com.sauci.app.widget, and repointed CODE_SIGN_ENTITLEMENTS to the
|
||||||
|
gitignored ios/.targets/LiveDrawWidget/generated.entitlements.
|
||||||
|
2. Android manifest lost the tools:node="remove" attributes on
|
||||||
|
READ_MEDIA_IMAGES and READ_MEDIA_VIDEO, so the built AAB regained
|
||||||
|
Play-forbidden permissions (verified via bundletool dump on the rejected
|
||||||
|
build 50 AAB).
|
||||||
|
3. iOS Info.plist gained dev-launcher-only keys NSBonjourServices
|
||||||
|
(_expo._tcp), NSLocalNetworkUsageDescription, and RCTMetroPort.
|
||||||
|
4. android/gradle.properties reset to stock -Xmx2048m
|
||||||
|
-XX:MaxMetaspaceSize=512m, causing Metaspace OOM in kspReleaseKotlin/CMake
|
||||||
|
under RN 0.85.
|
||||||
|
|
||||||
|
Separately, a package absent from the new SDK's bundledNativeModules
|
||||||
|
(expo-av) has no compatible release and fails the iOS archive (EXAV.h
|
||||||
|
imports removed ExpoModulesCore headers).
|
||||||
|
|
||||||
|
## Remediation
|
||||||
|
|
||||||
|
Fixed by restoring widget identity and tracking
|
||||||
|
targets/widget/generated.entitlements with a pinned bundleIdentifier
|
||||||
|
(604eec3); restoring manifest tools:node="remove" attributes (7aca39a);
|
||||||
|
removing the dev-launcher-only Info.plist keys (6bd04a6); raising
|
||||||
|
gradle.properties memory to 4096m/1024m (79233f5); and migrating expo-av to
|
||||||
|
expo-video/expo-audio (b8cb2add).
|
||||||
|
|
||||||
|
Extend apps/mobile/scripts/release-preflight.mjs with executable checks for
|
||||||
|
items 2 and 3 (item 1 is already covered by the existing LiveDrawWidget
|
||||||
|
preflight check; item 4 hard-fails the build by itself). Add an "After
|
||||||
|
native regeneration" checklist to docs/releasing.md.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
`node apps/mobile/scripts/release-preflight.mjs` fails if the tracked
|
||||||
|
AndroidManifest.xml lacks tools:node="remove" on READ_MEDIA_IMAGES or
|
||||||
|
READ_MEDIA_VIDEO, or if ios/Sauci/Info.plist contains NSBonjourServices,
|
||||||
|
NSLocalNetworkUsageDescription, or RCTMetroPort.
|
||||||
|
`node --test apps/mobile/scripts/release-preflight.test.mjs` covers the new
|
||||||
|
checks.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
id: 2026-08-31-local-eas-build-preconditions
|
||||||
|
date: 2026-08-31
|
||||||
|
status: promoted
|
||||||
|
scope: apps/mobile local EAS release builds
|
||||||
|
evidence: v1.0.8 local EAS build session, commit d8bedea
|
||||||
|
destination: docs/releasing.md
|
||||||
|
review_after: 2026-11-30
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Local EAS release builds failed before native compilation for environment
|
||||||
|
reasons unrelated to the app source, and a successful iOS build left the
|
||||||
|
tracked source in a state that fails release-preflight afterward.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
Run `eas build --local` from a non-interactive shell without first exporting
|
||||||
|
ANDROID_HOME/ANDROID_SDK_ROOT/JAVA_HOME: fails with "SDK location not
|
||||||
|
found" because non-interactive shells do not load the user profile. Run it
|
||||||
|
with a non-empty EAS_LOCAL_BUILD_WORKINGDIR: fails with "Workingdir is not
|
||||||
|
empty". Run it outside apps/mobile: eas cannot find the project. Pipe the
|
||||||
|
build log through `tail`: the full log needed for diagnosis is lost. After a
|
||||||
|
successful iOS build, EAS auto-increment updates both Info.plists but not
|
||||||
|
the widget target's CURRENT_PROJECT_VERSION in the pbxproj, so
|
||||||
|
release-preflight fails until it is synced.
|
||||||
|
|
||||||
|
## Cause
|
||||||
|
|
||||||
|
`eas` must be installed as a global CLI (scripts call bare `eas`; eas-cli
|
||||||
|
23.0.0 verified). Non-interactive shells used for automation do not source
|
||||||
|
the profile that normally exports the Android/Java environment variables.
|
||||||
|
EAS_LOCAL_BUILD_WORKINGDIR must not exist or be empty. EAS iOS
|
||||||
|
auto-increment writes plist versions but does not touch the widget target's
|
||||||
|
pbxproj CURRENT_PROJECT_VERSION, so it drifts from the app's build number by
|
||||||
|
one increment after every successful build.
|
||||||
|
|
||||||
|
## Remediation
|
||||||
|
|
||||||
|
Before a local EAS build: verify `eas` is on PATH, explicitly export
|
||||||
|
ANDROID_HOME, ANDROID_SDK_ROOT, and JAVA_HOME (JDK 17), ensure
|
||||||
|
EAS_LOCAL_BUILD_WORKINGDIR does not exist or is empty, and run from
|
||||||
|
apps/mobile. Capture the full build log to a file rather than piping through
|
||||||
|
`tail`. After a successful build, sync the widget's CURRENT_PROJECT_VERSION
|
||||||
|
to the new build number (commit d8bedea, sed 46 to 47) and sync
|
||||||
|
ios/Podfile.lock from the successful build's working directory when
|
||||||
|
dependencies changed, since the archive's pod install resolution is
|
||||||
|
authoritative and local CocoaPods here is broken.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
docs/releasing.md's local-build section documents the environment
|
||||||
|
preconditions and the post-build widget CURRENT_PROJECT_VERSION and
|
||||||
|
Podfile.lock sync steps; `npm run release:preflight` passes after the sync.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
id: 2026-08-31-mobile-release-preflight-and-attempts
|
||||||
|
date: 2026-08-31
|
||||||
|
status: promoted
|
||||||
|
scope: mobile production release preparation and local EAS builds
|
||||||
|
evidence: v1.0.7 Android and iOS local EAS release investigation and verified artifacts
|
||||||
|
destination: apps/mobile/scripts/release-preflight.mjs, apps/mobile/scripts/release-preflight.test.mjs, docs/releasing.md, and release skills
|
||||||
|
review_after: 2027-02-28
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
The v1.0.7 release spent repeated attempts on archive, plugin startup, package
|
||||||
|
installation, disk-space, signing, and iOS extension-version failures before
|
||||||
|
valid Android and iOS artifacts were accepted.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
Run a local production EAS build with an ignored public OTA certificate, a
|
||||||
|
package-directory plugin path, insufficient disk space, or divergent widget
|
||||||
|
`CURRENT_PROJECT_VERSION`. Each condition fails or produces an invalid input
|
||||||
|
before a valid final artifact can be accepted.
|
||||||
|
|
||||||
|
## Cause
|
||||||
|
|
||||||
|
Pre-compilation setup failures were treated as rebuild failures. Source parity,
|
||||||
|
archive inclusion, local EAS plugin shape, disk capacity, and effective iOS widget
|
||||||
|
build settings were not checked together before EAS allocation.
|
||||||
|
|
||||||
|
## Remediation
|
||||||
|
|
||||||
|
Run the deterministic mobile release preflight before every production build.
|
||||||
|
Classify attempts before retrying: archive, plugin, npm, and ENOSPC failures do
|
||||||
|
not consume a native build; signing, profile, and capability failures require
|
||||||
|
provider read-back before one replacement build. Retain work directories and logs
|
||||||
|
at stable ignored paths. Accept only independently inspected artifacts and keep
|
||||||
|
rejected artifacts out of the upload path.
|
||||||
|
|
||||||
|
The Android native input failure was a Gradle task ordering issue: Prefab inputs
|
||||||
|
must be available before dependent native tasks. Preserve the explicit ordering
|
||||||
|
fix rather than removing Firebase or changing dependency versions without proof.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
The preflight node tests reject an ignored OTA certificate and an effective widget
|
||||||
|
build-setting mismatch. The checked-in source invariants pass the preflight. The
|
||||||
|
maintained release guide and skills record local EAS path requirements, provider
|
||||||
|
read-back, phase-specific status language, artifact inspection, store read-back,
|
||||||
|
and scoped upload authorization boundaries.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
id: 2026-08-31-npm-lockfile-divergence
|
||||||
|
date: 2026-08-31
|
||||||
|
status: promoted
|
||||||
|
scope: root package.json, package-lock.json, apps/web dependencies, CI
|
||||||
|
evidence: v1.0.8 lockfile investigation, commits 90cde4a, 9609a90
|
||||||
|
destination: docs/development/dependencies.md
|
||||||
|
review_after: 2026-11-30
|
||||||
|
---
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
CI and the API Docker image (node 20.19.4/npm 10.8.2, per .nvmrc) broke
|
||||||
|
invisibly while a lockfile regenerated on this dev machine (node 26/npm
|
||||||
|
11.19) passed local `npm ci`. Pushes to staging bypass required status
|
||||||
|
checks, so the red CI ran unnoticed for hours.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
Regenerate package-lock.json with npm 11 on a wildcard-range dependency
|
||||||
|
(react-native-worklets depends on `@react-native/metro-config: "*"`), then
|
||||||
|
run `npm ci` under node 20.19.4/npm 10.8.2. npm 10 requires 0.85.3 at that
|
||||||
|
tree position; npm 11 resolves/hoists 0.87.x there. `npm ci` fails with
|
||||||
|
"Invalid: lock file's ... does not satisfy". The reverse direction also
|
||||||
|
fails: a lock regenerated by npm 10 fails `npm ci` under npm 11 with missing
|
||||||
|
platform-optional binaries (@next/swc-linux-x64-gnu breaking `next build`,
|
||||||
|
@rollup/rollup-linux-x64-gnu breaking the API Docker tsup build, lightningcss
|
||||||
|
variants breaking local npm ci), matching the npm/cli#4828-style
|
||||||
|
optional-dependency resolution gap.
|
||||||
|
|
||||||
|
## Cause
|
||||||
|
|
||||||
|
Two different npm major versions in active use (CI/Docker on npm 10, this
|
||||||
|
dev machine on npm 11) resolve wildcard/range dependencies to different
|
||||||
|
versions at the same tree position, and each npm's own platform-optional
|
||||||
|
binary selection is not portable to the other npm's lockfile.
|
||||||
|
|
||||||
|
## Remediation
|
||||||
|
|
||||||
|
Pin the wildcard-range dependency via a root package.json override
|
||||||
|
(`"@react-native/metro-config": "0.85.3"`, alongside the existing
|
||||||
|
rollup/react-native/expo pins). Pin apps/web `@next/swc` linux/darwin
|
||||||
|
binaries as direct optionalDependencies (must be bumped together with any
|
||||||
|
`next` version bump). Regenerate the lock and validate it both ways: local
|
||||||
|
`npm ci` plus `npm run verify:fast`, and
|
||||||
|
`docker run --platform linux/amd64 node:20.19.4 ... npm ci` (the
|
||||||
|
`--platform` flag is mandatory; Apple Silicon Docker otherwise runs arm64
|
||||||
|
and the platform-binary check is meaningless). After any push that bypassed
|
||||||
|
required status checks, check `gh run list --branch staging`.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
commits 90cde4a and 9609a90 add the override and optionalDependencies pins
|
||||||
|
and record a lockfile that passes both the amd64-emulated `npm ci` under
|
||||||
|
node 20.19.4 and local `npm ci` plus `npm run verify:fast`.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: release-mobile
|
name: release-mobile
|
||||||
description: Prepare and verify Sauci iOS or Android releases and store uploads.
|
description: Prepare, verify, and ship Sauci iOS or Android releases end to end, including store uploads, review submission, and rollout.
|
||||||
---
|
---
|
||||||
|
|
||||||
Read `.codex/skills/release-mobile/SKILL.md` completely and follow it.
|
Read `.codex/skills/release-mobile/SKILL.md` completely and follow it.
|
||||||
@@ -13,9 +13,13 @@ drafting notes and [references/artifact-verification.md](references/artifact-ver
|
|||||||
before accepting a build.
|
before accepting a build.
|
||||||
|
|
||||||
A request to generate or create a release authorizes the scoped version edits,
|
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
|
release notes, local builds, release commits, and one local annotated tag. When
|
||||||
does not authorize pushing commits or tags, uploading binaries, submitting a
|
the request also asks to ship, upload, submit, or release to the stores, it
|
||||||
store release, changing signing credentials, or regenerating native projects.
|
authorizes the complete store pipeline for the stated platforms after the exact
|
||||||
|
artifact has been verified: upload, App Review / Google Play review submission,
|
||||||
|
and rollout, including full production rollout (see the `release-mobile` skill).
|
||||||
|
It does not authorize pushing commits or tags, changing signing credentials, or
|
||||||
|
regenerating native projects unless the request explicitly includes that action.
|
||||||
|
|
||||||
## Defaults
|
## Defaults
|
||||||
|
|
||||||
@@ -39,6 +43,8 @@ Require all of the following:
|
|||||||
- consistent public and OTA runtime versions across `app.json`, explicit
|
- consistent public and OTA runtime versions across `app.json`, explicit
|
||||||
`app.config.js` runtime, Android, and every iOS target
|
`app.config.js` runtime, Android, and every iOS target
|
||||||
(`scripts/set_version.mjs --check`);
|
(`scripts/set_version.mjs --check`);
|
||||||
|
- `npm run release:preflight -w @sauci/mobile`, which also checks the iOS widget
|
||||||
|
effective build settings and OTA certificate archive inclusion;
|
||||||
- Node 20, JDK 17 for Android, and Xcode/CocoaPods for iOS;
|
- Node 20, JDK 17 for Android, and Xcode/CocoaPods for iOS;
|
||||||
- the configured production API/Auth/RevenueCat environment, without printing
|
- the configured production API/Auth/RevenueCat environment, without printing
|
||||||
secret values.
|
secret values.
|
||||||
@@ -80,6 +86,15 @@ Build from the clean prepared source using the package scripts in
|
|||||||
versioned artifact directory. When building both platforms, build Android first,
|
versioned artifact directory. When building both platforms, build Android first,
|
||||||
then iOS.
|
then iOS.
|
||||||
|
|
||||||
|
For local EAS, retain the working directory and logs under a stable ignored path,
|
||||||
|
set `EAS_LOCAL_BUILD_ARTIFACTS_DIR`, and preflight available disk space before
|
||||||
|
starting. Set `EAS_LOCAL_BUILD_PLUGIN_PATH` only to the matching plugin
|
||||||
|
executable `bin/run`, never a package directory or a hardcoded cache location.
|
||||||
|
Classify plugin, archive, npm, and ENOSPC failures as pre-compilation failures;
|
||||||
|
repair and preflight them without spending another build. Make at most one
|
||||||
|
meaningful build for each newly proven input state. After a signing, profile, or
|
||||||
|
capability failure, read back and prove the provider correction before rebuilding.
|
||||||
|
|
||||||
After each build:
|
After each build:
|
||||||
|
|
||||||
1. Inspect the worktree before doing anything else. Accept only the documented
|
1. Inspect the worktree before doing anything else. Accept only the documented
|
||||||
@@ -88,15 +103,19 @@ After each build:
|
|||||||
dependency, or native-project regeneration as a failed build.
|
dependency, or native-project regeneration as a failed build.
|
||||||
2. Independently inspect the binary for package/bundle identifier, public
|
2. Independently inspect the binary for package/bundle identifier, public
|
||||||
version, platform build number, production endpoint configuration where
|
version, platform build number, production endpoint configuration where
|
||||||
inspectable, and signing identity/fingerprint. Require the embedded public
|
inspectable, signing identity/fingerprint, OTA runtime/channel/certificate,
|
||||||
version to equal the requested version.
|
and SHA-256. Require the embedded public version to equal the requested
|
||||||
|
version. For Android, also inspect all required ABIs, Worklets/Reanimated/Skia
|
||||||
|
libraries, and the release permissions. For iOS, inspect every packaged
|
||||||
|
extension after export.
|
||||||
3. Record the verified platform build number in the new release-note entry.
|
3. Record the verified platform build number in the new release-note entry.
|
||||||
4. Commit only the expected metadata and release-note update as
|
4. Commit only the expected metadata and release-note update as
|
||||||
`chore(<platform>): record v<version> build <number>`. The worktree must be
|
`chore(<platform>): record v<version> build <number>`. The worktree must be
|
||||||
clean before starting the next platform build.
|
clean before starting the next platform build.
|
||||||
|
|
||||||
Do not accept a successful EAS exit code as proof of a valid artifact. If either
|
Do not accept a successful EAS exit code as proof of a valid artifact. Quarantine
|
||||||
requested platform fails, do not tag a partial release.
|
or remove rejected artifacts so only accepted paths can be offered for upload. If
|
||||||
|
either requested platform fails, do not tag a partial release.
|
||||||
|
|
||||||
## Tag and prove the result
|
## Tag and prove the result
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: release-mobile
|
name: release-mobile
|
||||||
description: Prepare and verify Sauci iOS or Android releases. Use for versioning, EAS/native builds, Fastlane upload, release notes, or store submission preparation.
|
description: Prepare, verify, and ship Sauci iOS or Android releases end to end. Use for versioning, EAS/native builds, store uploads, App Review / Play review submission, and rollout.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mobile release
|
# Mobile release
|
||||||
@@ -9,6 +9,41 @@ Read `docs/releasing.md`, `apps/mobile/RELEASING.md` when present, and the mobil
|
|||||||
scoped instructions. Confirm target platform and release profile. Run repository
|
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.
|
||||||
|
|
||||||
|
## OTA update vs full store release
|
||||||
|
|
||||||
|
Choose the delivery mechanism before doing anything else; they are different
|
||||||
|
pipelines with different blast radii. `docs/mobile-ota.md` is the authority.
|
||||||
|
|
||||||
|
- **OTA update** (`npm run ota:publish:<env>` in `apps/mobile`): ships only the
|
||||||
|
JavaScript bundle and assets to builds already installed on devices. No store
|
||||||
|
review, live within minutes, and rolled back by repointing the XPREM channel
|
||||||
|
at a previous update. Valid only when the change touches no native code, no
|
||||||
|
native dependency, no `ios/`/`android/` project files, and no config that
|
||||||
|
alters the native binary. The OTA runtime version equals the public app
|
||||||
|
version, so an OTA update can only reach installs of the current
|
||||||
|
`expo.version`.
|
||||||
|
- **Full store release** (this skill): a new signed binary through App Store
|
||||||
|
Connect and Google Play, including review. Required whenever native code,
|
||||||
|
native dependencies, the checked-in native projects, Expo SDK, permissions,
|
||||||
|
entitlements, or `expo.version` change. Rollback is coarser: Play staged
|
||||||
|
rollouts can be halted, iOS phased release can be paused, and a bad release is
|
||||||
|
otherwise superseded by shipping the next version.
|
||||||
|
|
||||||
|
When a change qualifies for OTA, prefer OTA unless the user asks for a store
|
||||||
|
release. Never present an OTA publish as a store release or vice versa; report
|
||||||
|
which mechanism was used.
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
An explicit request for a mobile release authorizes the complete pipeline for
|
||||||
|
the stated platforms: version bump, build, verification, store upload, review
|
||||||
|
submission (App Review / Google Play review), and rollout — including full
|
||||||
|
production rollout. Rollout is recoverable by design (staged-rollout halt,
|
||||||
|
phased-release pause, OTA supersession, or the next store version), so do not
|
||||||
|
pause to re-confirm these steps; execute them and report. A request that
|
||||||
|
explicitly narrows scope (for example "upload only", "TestFlight only", or a
|
||||||
|
named track) still bounds what you do.
|
||||||
|
|
||||||
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. The `generate-release` skill may instead
|
and prove `HEAD` matches its upstream. The `generate-release` skill may instead
|
||||||
build a clean, scoped release-preparation commit that descends directly from that
|
build a clean, scoped release-preparation commit that descends directly from that
|
||||||
@@ -17,40 +52,65 @@ 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
|
so routine release builds must package those checked-in sources. Do not run
|
||||||
`expo prebuild` first; native regeneration is a separate reviewed change.
|
`expo prebuild` first; native regeneration is a separate reviewed change.
|
||||||
|
|
||||||
Use the package scripts in `apps/mobile/package.json`; they create artifacts only.
|
Run `npm run release:preflight -w @sauci/mobile` before allocating an EAS build.
|
||||||
Never infer upload or submission authority from build authorization. Store upload
|
It rejects inconsistent public/runtime versions, an iOS widget build-setting
|
||||||
uses a separately authorized command.
|
override, and a public OTA certificate that would be omitted from the archive.
|
||||||
|
For a local EAS build, also run `npm run release:preflight:local-eas -w
|
||||||
|
@sauci/mobile` after setting `EAS_LOCAL_BUILD_PLUGIN_PATH` to the matching
|
||||||
|
plugin's executable `bin/run`. Do not point it at the package directory or
|
||||||
|
hardcode a machine-local cache path.
|
||||||
|
|
||||||
## Upload authorization gate
|
Classify a failure before rebuilding. Archive, plugin-spawn, package-install, and
|
||||||
|
disk-space failures happen before native compilation and must be repaired and
|
||||||
|
preflighted without consuming another build. After a signing, profile, or
|
||||||
|
capability failure, read back the provider state and prove only the affected
|
||||||
|
input changed before rebuilding. Retain the EAS work directory, command log, and
|
||||||
|
stable artifact path for every meaningful attempt. Never retry an ambiguous
|
||||||
|
upload.
|
||||||
|
|
||||||
After an artifact passes verification, stop immediately before the external
|
Report provider progress by phase: packaging, native compilation, submission
|
||||||
upload and ask the user for confirmation. The request must identify:
|
queue, or store processing. Do not describe every failure as a rebuild.
|
||||||
|
|
||||||
|
Use the package scripts in `apps/mobile/package.json` to build artifacts and the
|
||||||
|
Fastlane lanes in `apps/mobile/fastlane/Fastfile` to upload, submit, and roll
|
||||||
|
out.
|
||||||
|
|
||||||
|
## Upload and submission execution
|
||||||
|
|
||||||
|
Verification before upload is unchanged and mandatory. Independently verify and
|
||||||
|
report:
|
||||||
|
|
||||||
- the platform, exact artifact path, bundle/package identifier, public version,
|
- the platform, exact artifact path, bundle/package identifier, public version,
|
||||||
build number, signature identity, and SHA-256 digest;
|
build number, signature identity, and SHA-256 digest;
|
||||||
- the exact destination and resulting state: Google Play internal-track draft or
|
- the exact destination and resulting state: which Play track and rollout
|
||||||
App Store Connect/TestFlight processing;
|
status, or App Store Connect version state and whether App Review submission
|
||||||
- that the upload does not authorize a public rollout or App Review submission.
|
and automatic release are included.
|
||||||
|
|
||||||
For iOS, inspect the current App Store Connect automatic-distribution behavior
|
Once these details match the request, proceed through upload, review
|
||||||
before asking. If the build would be distributed automatically, name the exact
|
submission, and rollout without requesting further confirmation.
|
||||||
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
|
- **Android**: `bundle exec fastlane android release aab:<absolute path>`
|
||||||
submitting, require this fresh confirmation once the exact verified artifact and
|
commits the release on the production track (pass `track:` to target another
|
||||||
destination are known. End the request with: `Upload this verified artifact to
|
track, `rollout:0.1` for a staged rollout). Play review runs automatically;
|
||||||
<destination> now?` A clear affirmative response authorizes one upload of that
|
rollout proceeds on approval. Use the `upload` lane only when the request is
|
||||||
artifact to that destination, plus only any automatic TestFlight distribution
|
explicitly upload-only.
|
||||||
explicitly disclosed in the request. Run `eas submit` with the explicit
|
- **iOS**: `bundle exec fastlane ios release ipa:<absolute path>` uploads,
|
||||||
`--platform`, `--profile production`, `--path`, and `--wait` arguments; never
|
creates the App Store version, submits for App Review, and releases
|
||||||
select `--latest`. If the result is ambiguous, inspect EAS and the store before
|
automatically on approval (pass `phased_release:true` for a 7-day phased
|
||||||
doing anything else, do not retry, and ask again before another upload.
|
release, `release_notes:"..."` for What's New). Use the `upload` lane for
|
||||||
|
TestFlight-only requests. `eas submit` remains valid for upload-only flows;
|
||||||
|
when used, pass explicit `--platform`, `--profile production`, `--path`, and
|
||||||
|
`--wait` — never `--latest`.
|
||||||
|
|
||||||
Apply this gate to every upload method, including EAS, Fastlane, Xcode Organizer,
|
App Store Connect API group association does not establish the automatic-
|
||||||
and the store consoles. A later public rollout, App Review submission, or
|
distribution switch. Verify that switch in an authenticated App Store Connect UI
|
||||||
additional TestFlight distribution requires another fresh authorization.
|
session before upload; if that UI verification is unavailable, stop rather
|
||||||
|
than inferring the distribution effect from API data.
|
||||||
|
|
||||||
|
If an upload or submission command returns an ambiguous result, inspect EAS and
|
||||||
|
the store before doing anything else; do not retry blind. After submission, read
|
||||||
|
back the store state (Play Console track status / App Store Connect version
|
||||||
|
state) and report it.
|
||||||
|
|
||||||
Preserve platform signing and local EAS versioning. After a build, verify that
|
Preserve platform signing and local EAS versioning. After a build, verify that
|
||||||
tracked changes are limited to the expected version metadata, then independently
|
tracked changes are limited to the expected version metadata, then independently
|
||||||
@@ -58,5 +118,30 @@ read the artifact version and signature. For an iOS IPA, inspect every packaged
|
|||||||
app extension as well as the containing app and require matching
|
app extension as well as the containing app and require matching
|
||||||
`CFBundleShortVersionString` and `CFBundleVersion` values. Treat Xcode archive
|
`CFBundleShortVersionString` and `CFBundleVersion` values. Treat Xcode archive
|
||||||
warnings as diagnostic evidence, not the final artifact state: decide whether to
|
warnings as diagnostic evidence, not the final artifact state: decide whether to
|
||||||
reject or rebuild only after reading the exported IPA itself. Report the artifact,
|
reject or rebuild only after reading the exported IPA itself. Report the
|
||||||
version/build number, upload result when authorized, and store read-back state.
|
artifact, version/build number, upload and submission results, rollout state,
|
||||||
|
and store read-back state.
|
||||||
|
|
||||||
|
For Android, verify the accepted AAB has all four required ABIs, the Worklets,
|
||||||
|
Reanimated, and Skia native libraries, no broad `READ_MEDIA_IMAGES` or
|
||||||
|
`READ_MEDIA_VIDEO` permissions, and the production OTA runtime, channel, and
|
||||||
|
public certificate. For iOS, verify the exported IPA, not archive warnings, and
|
||||||
|
check both the containing app and every extension.
|
||||||
|
|
||||||
|
For the Android native dependency ordering already tracked in Gradle, preserve
|
||||||
|
the explicit task dependency that makes Prefab inputs available before dependent
|
||||||
|
native tasks. Fix task validation with ordering, not Firebase removal or
|
||||||
|
dependency-version roulette.
|
||||||
|
|
||||||
|
After Android upload, EAS Submit `FINISHED`, the exact internal/DRAFT/no-rollout
|
||||||
|
submit configuration, and the retained artifact digest are the available
|
||||||
|
authoritative read-back when a direct Play track read would require creating an
|
||||||
|
edit. Never create an edit merely to claim read-only verification.
|
||||||
|
|
||||||
|
For a checked-in iOS entitlement, inspect the real Apple App ID capability state
|
||||||
|
before a signing rebuild. The capability sync bug tracked in
|
||||||
|
https://github.com/expo/eas-cli/issues/3986 can report success while Apple
|
||||||
|
rejects a malformed update. Read back Associated Domains, regenerate only the
|
||||||
|
affected profile, and decode that profile to prove the entitlement before
|
||||||
|
rebuilding. Noninteractive managed App Store Connect key auth may require
|
||||||
|
`EXPO_APPLE_TEAM_ID`; preserve certificates and unrelated target profiles.
|
||||||
@@ -39,3 +39,11 @@ terraform
|
|||||||
.claude
|
.claude
|
||||||
.codex
|
.codex
|
||||||
.ralph
|
.ralph
|
||||||
|
|
||||||
|
# The admin app builds from this same monorepo context (apps/admin/Dockerfile).
|
||||||
|
!apps/admin
|
||||||
|
!apps/admin/**
|
||||||
|
|
||||||
|
# Host build artefacts must never leak into any image context.
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
@@ -24,9 +24,14 @@ jobs:
|
|||||||
echo "master is frozen; target staging or main" >&2
|
echo "master is frozen; target staging or main" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ "$BASE_BRANCH" = "main" ] && [ "$HEAD_BRANCH" != "staging" ]; then
|
if [ "$BASE_BRANCH" = "main" ]; then
|
||||||
echo "Only staging may be promoted to main" >&2
|
case "$HEAD_BRANCH" in
|
||||||
exit 1
|
staging|hotfix/*|release/*) ;;
|
||||||
|
*)
|
||||||
|
echo "main takes staging promotions, or a hotfix/* or release/* branch for direct work" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
if [ "$BASE_BRANCH" != "staging" ] && [ "$BASE_BRANCH" != "main" ]; then
|
if [ "$BASE_BRANCH" != "staging" ] && [ "$BASE_BRANCH" != "main" ]; then
|
||||||
echo "Feature pull requests target staging; promotions target main" >&2
|
echo "Feature pull requests target staging; promotions target main" >&2
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ environment limitation; do not call it a passing product check.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| System boundaries and data flow | `docs/architecture/system.md` |
|
| System boundaries and data flow | `docs/architecture/system.md` |
|
||||||
| Local services, ports, fixtures | `docs/development/local-stack.md` |
|
| Local services, ports, fixtures | `docs/development/local-stack.md` |
|
||||||
|
| Dependency and lockfile discipline | `docs/development/dependencies.md` |
|
||||||
| Test layers and required gates | `docs/testing/strategy.md` |
|
| Test layers and required gates | `docs/testing/strategy.md` |
|
||||||
| Database migration safety | `docs/operations/database-migrations.md` |
|
| Database migration safety | `docs/operations/database-migrations.md` |
|
||||||
| Agent roles and effort routing | `docs/agents/routing.md` |
|
| Agent roles and effort routing | `docs/agents/routing.md` |
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { featureInterestKey, formatFeatureName, toRecentFeatureInterest } from './featureInterests';
|
||||||
|
|
||||||
|
describe('toRecentFeatureInterest', () => {
|
||||||
|
it('preserves the standalone API feature field for dashboard rendering', () => {
|
||||||
|
const interest = toRecentFeatureInterest({
|
||||||
|
user_id: 'user-1',
|
||||||
|
feature: 'better_chat',
|
||||||
|
created_at: '2026-09-05T12:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(interest).toEqual({
|
||||||
|
user_id: 'user-1',
|
||||||
|
feature: 'better_chat',
|
||||||
|
created_at: '2026-09-05T12:00:00.000Z',
|
||||||
|
});
|
||||||
|
expect(formatFeatureName(interest.feature)).toBe('Better Chat');
|
||||||
|
expect(featureInterestKey(interest)).toBe('user-1:better_chat');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export interface FeatureInterestRow {
|
||||||
|
user_id: string;
|
||||||
|
feature: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecentFeatureInterest extends FeatureInterestRow {
|
||||||
|
profile?: {
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
email: string | null;
|
||||||
|
avatar_url: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRecentFeatureInterest(row: FeatureInterestRow): RecentFeatureInterest {
|
||||||
|
return {
|
||||||
|
user_id: row.user_id,
|
||||||
|
feature: row.feature,
|
||||||
|
created_at: row.created_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function featureInterestKey(row: Pick<FeatureInterestRow, 'user_id' | 'feature'>): string {
|
||||||
|
return `${row.user_id}:${row.feature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFeatureName(feature: string): string {
|
||||||
|
return feature
|
||||||
|
.split('_')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
@@ -10,36 +10,22 @@ import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip } from 'recha
|
|||||||
import { format, subDays, startOfDay, endOfDay, formatDistanceToNow } from 'date-fns';
|
import { format, subDays, startOfDay, endOfDay, formatDistanceToNow } from 'date-fns';
|
||||||
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
||||||
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
|
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
|
||||||
|
import {
|
||||||
|
formatFeatureName,
|
||||||
|
featureInterestKey,
|
||||||
|
toRecentFeatureInterest,
|
||||||
|
type FeatureInterestRow,
|
||||||
|
type RecentFeatureInterest,
|
||||||
|
} from '@/lib/featureInterests';
|
||||||
|
|
||||||
type FeatureInterestChartPoint = { date: string; count: number };
|
type FeatureInterestChartPoint = { date: string; count: number };
|
||||||
|
|
||||||
type RecentFeatureInterest = {
|
|
||||||
id: string;
|
|
||||||
user_id: string;
|
|
||||||
feature_name: string;
|
|
||||||
created_at: string;
|
|
||||||
profile?: {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
avatar_url: string | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type FeatureInterestCountsRow = {
|
type FeatureInterestCountsRow = {
|
||||||
feature_name: string;
|
feature_name: string;
|
||||||
opt_in_count: number;
|
opt_in_count: number;
|
||||||
opt_in_count_last_7_days: number;
|
opt_in_count_last_7_days: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatFeatureName(featureName: string) {
|
|
||||||
return featureName
|
|
||||||
.split('_')
|
|
||||||
.filter(Boolean)
|
|
||||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const canViewUsers = hasPermission(PERMISSION_KEYS.VIEW_USERS);
|
const canViewUsers = hasPermission(PERMISSION_KEYS.VIEW_USERS);
|
||||||
@@ -95,7 +81,7 @@ export function DashboardPage() {
|
|||||||
? adminData.from('feature_interests').select('created_at, user_id').gte('created_at', startDate.toISOString()).lte('created_at', endDate.toISOString())
|
? adminData.from('feature_interests').select('created_at, user_id').gte('created_at', startDate.toISOString()).lte('created_at', endDate.toISOString())
|
||||||
: Promise.resolve({ data: [] }),
|
: Promise.resolve({ data: [] }),
|
||||||
canViewUsers
|
canViewUsers
|
||||||
? adminData.from('feature_interests').select('id, user_id, feature_name, created_at').order('created_at', { ascending: false }).limit(10)
|
? adminData.from<FeatureInterestRow>('feature_interests').select('user_id, feature, created_at').order('created_at', { ascending: false }).limit(10)
|
||||||
: Promise.resolve({ data: [] }),
|
: Promise.resolve({ data: [] }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -158,12 +144,7 @@ export function DashboardPage() {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(item => ({
|
let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(toRecentFeatureInterest);
|
||||||
id: item.id,
|
|
||||||
user_id: item.user_id,
|
|
||||||
feature_name: item.feature_name,
|
|
||||||
created_at: item.created_at,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (canViewUsers && recentActivity.length > 0) {
|
if (canViewUsers && recentActivity.length > 0) {
|
||||||
const userIds = Array.from(new Set(recentActivity.map(item => item.user_id)));
|
const userIds = Array.from(new Set(recentActivity.map(item => item.user_id)));
|
||||||
@@ -579,14 +560,14 @@ export function DashboardPage() {
|
|||||||
if (item.profile?.name && item.profile.email) {
|
if (item.profile?.name && item.profile.email) {
|
||||||
subtitleParts.push(item.profile.email);
|
subtitleParts.push(item.profile.email);
|
||||||
}
|
}
|
||||||
subtitleParts.push(formatFeatureName(item.feature_name));
|
subtitleParts.push(formatFeatureName(item.feature));
|
||||||
|
|
||||||
const fallbackChar = (item.profile?.name || item.profile?.email || 'U')
|
const fallbackChar = (item.profile?.name || item.profile?.email || 'U')
|
||||||
.charAt(0)
|
.charAt(0)
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className="flex items-center justify-between gap-4 rounded-xl bg-white/5 p-3">
|
<div key={featureInterestKey(item)} className="flex items-center justify-between gap-4 rounded-xl bg-white/5 p-3">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage src={item.profile?.avatar_url || undefined} />
|
<AvatarImage src={item.profile?.avatar_url || undefined} />
|
||||||
|
|||||||
@@ -2,6 +2,21 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: 'expo',
|
extends: 'expo',
|
||||||
ignorePatterns: ['/dist/*'],
|
ignorePatterns: ['/dist/*'],
|
||||||
|
rules: {
|
||||||
|
// eslint-config-expo (SDK 56) pulls eslint-plugin-react-hooks v7, whose
|
||||||
|
// recommended config adds React Compiler era rules as errors. Existing
|
||||||
|
// patterns this codebase relies on deliberately (the "latest value" ref
|
||||||
|
// pattern, effect-driven setState, dynamically created inline
|
||||||
|
// components, and manual memoization) trip these new rules throughout
|
||||||
|
// the app. Adopting the stricter rules is a deliberate follow-up
|
||||||
|
// refactor, not an SDK upgrade side effect, so they are downgraded to
|
||||||
|
// warnings here.
|
||||||
|
'react-hooks/refs': 'warn',
|
||||||
|
'react-hooks/set-state-in-effect': 'warn',
|
||||||
|
'react-hooks/immutability': 'warn',
|
||||||
|
'react-hooks/preserve-manual-memoization': 'warn',
|
||||||
|
'react-hooks/static-components': 'warn',
|
||||||
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['plugins/**/*.js'],
|
files: ['plugins/**/*.js'],
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ expo-env.d.ts
|
|||||||
*.keystore
|
*.keystore
|
||||||
# Fastlane
|
# Fastlane
|
||||||
fastlane-key.json
|
fastlane-key.json
|
||||||
|
fastlane/.env
|
||||||
fastlane/report.xml
|
fastlane/report.xml
|
||||||
fastlane/README.md
|
fastlane/README.md
|
||||||
@@ -13,3 +13,5 @@ native iOS/Android targets.
|
|||||||
- Add stable accessibility labels or `testID` values for critical interactions.
|
- Add stable accessibility labels or `testID` values for critical interactions.
|
||||||
- Verify focused Jest tests, typecheck, lint, then native/Expo behavior as risk
|
- Verify focused Jest tests, typecheck, lint, then native/Expo behavior as risk
|
||||||
requires. UI work needs observable simulator evidence.
|
requires. UI work needs observable simulator evidence.
|
||||||
|
- Before and after any `expo prebuild`, follow the "After native regeneration"
|
||||||
|
checklist in `docs/releasing.md`.
|
||||||
@@ -12,5 +12,8 @@ local.properties
|
|||||||
*.hprof
|
*.hprof
|
||||||
.cxx/
|
.cxx/
|
||||||
|
|
||||||
|
# generated inline modules
|
||||||
|
app/src/main/java/inline/
|
||||||
|
|
||||||
# Bundle artifacts
|
# Bundle artifacts
|
||||||
*.jsbundle
|
*.jsbundle
|
||||||
@@ -11,7 +11,7 @@ def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
|||||||
react {
|
react {
|
||||||
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||||
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||||
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
|
hermesCommand = new File(["node", "--print", "require.resolve('hermes-compiler/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/hermesc/%OS-BIN%/hermesc"
|
||||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||||
|
|
||||||
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||||
@@ -64,7 +64,7 @@ react {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set this to true in release builds to optimize the app using R8.
|
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||||
*/
|
*/
|
||||||
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||||
|
|
||||||
@@ -92,8 +92,9 @@ 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 49
|
versionCode 51
|
||||||
versionName "1.0.7"
|
versionName "1.0.8"
|
||||||
|
|
||||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -181,54 +182,4 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
apply plugin: 'com.google.firebase.crashlytics'
|
apply plugin: 'com.google.firebase.crashlytics'
|
||||||
|
|
||||||
// Reanimated 4.1.7 imports Worklets from its pre-AGP-8 CMake output path,
|
|
||||||
// while AGP 8.14 writes release outputs below intermediates/cxx/RelWithDebInfo.
|
|
||||||
// Keep the checked-in Android project compatible with the supported SDK 54
|
|
||||||
// dependency set by materializing that one legacy input after Worklets builds.
|
|
||||||
def workletsReleaseAbis = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]
|
|
||||||
def syncWorkletsReleaseCmakeInputs = tasks.register("syncWorkletsReleaseCmakeInputs") {
|
|
||||||
dependsOn(":react-native-worklets:externalNativeBuildRelease")
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
def workletsIntermediates = file("${rootDir}/../../../node_modules/react-native-worklets/android/build/intermediates")
|
|
||||||
workletsReleaseAbis.each { abi ->
|
|
||||||
def libraries = fileTree(workletsIntermediates) {
|
|
||||||
include "cxx/RelWithDebInfo/**/obj/${abi}/libworklets.so"
|
|
||||||
}.files
|
|
||||||
if (libraries.size() != 1) {
|
|
||||||
throw new GradleException("Expected exactly one Worklets release library for ${abi}, found ${libraries.size()}")
|
|
||||||
}
|
|
||||||
def source = libraries.iterator().next()
|
|
||||||
def destination = file("${workletsIntermediates}/cmake/release/obj/${abi}/libworklets.so")
|
|
||||||
destination.parentFile.mkdirs()
|
|
||||||
java.nio.file.Files.deleteIfExists(destination.toPath())
|
|
||||||
source.withInputStream { input ->
|
|
||||||
destination.withOutputStream { output -> output << input }
|
|
||||||
}
|
|
||||||
if (destination.length() != source.length()) {
|
|
||||||
throw new GradleException("Failed to copy Worklets release library for ${abi}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gradle.projectsEvaluated {
|
|
||||||
def reanimatedProject = findProject(":react-native-reanimated")
|
|
||||||
reanimatedProject?.tasks?.configureEach { task ->
|
|
||||||
if (task.name == "externalNativeBuildRelease" || task.name == "prefabReleasePackage") {
|
|
||||||
task.dependsOn(syncWorkletsReleaseCmakeInputs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.configureEach { task ->
|
|
||||||
if (task.name.startsWith("configureCMakeRelWithDebInfo")) {
|
|
||||||
task.dependsOn(
|
|
||||||
":react-native-worklets:prefabReleasePackage",
|
|
||||||
":react-native-reanimated:prefabReleasePackage",
|
|
||||||
":shopify_react-native-skia:prefabReleasePackage",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
|
||||||
|
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||||
|
</manifest>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||||
<uses-permission android:name="com.android.vending.BILLING"/>
|
<uses-permission android:name="com.android.vending.BILLING"/>
|
||||||
<queries>
|
<queries>
|
||||||
<package android:name="com.instagram.android"/>
|
<package android:name="com.instagram.android"/>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<data android:scheme="https"/>
|
<data android:scheme="https"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:requestLegacyExternalStorage="true" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules" android:enableOnBackInvokedCallback="false">
|
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:requestLegacyExternalStorage="true" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
|
||||||
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
|
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
|
||||||
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
|
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
|
||||||
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
|
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
|
||||||
@@ -30,13 +30,14 @@
|
|||||||
<meta-data android:name="expo.modules.updates.CODE_SIGNING_CERTIFICATE" android:value="-----BEGIN CERTIFICATE-----
MIICzTCCAbWgAwIBAgIJAPrPjxnUOGisMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV
BAMTBVNhdWNpMB4XDTI2MDgzMDE4MTAzMFoXDTM2MDgzMDE4MTAzMFowEDEOMAwG
A1UEAxMFU2F1Y2kwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWiUac
8i9bIbNZjv8iunVr6kqlgb9vea98JHz/A83biyszg0+GpmLYsr4zV2F7A6RitiIx
SOML+wVedyD0F2quWccBGXinvMAds0TxgIOjQfP+8+VJ/duDjveHvn+OFW4s7XxW
SY7TK1ufDcPWo/f5XzfqtBveug49zv3/F3MwfmMUbEY/zvH7nayPGJNuSyf3wISn
JIt0BsHNhWykRXCzjaF2fy3nAebOyezbtbapCjUlxPENVtBCzBQ7C9kaQXCBw5mV
hxhJlOxHWXpHJgDLHpev8A/CWQRh67tsevjKi6R6SDlf0R0xSbDJTElx8goWOOQW
VO9aDqI+oH0KpWfnAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E
DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAyUc20bIRCxoBTJg84mPl
2nFDWSaAfzgo0K0b6x2T1suuq4LAOUgzBqJISrQAALd0besK1qUZSAMhjQu1wPZT
MHuWRMxQHfOkyHMYUfOoPMS4bE+4K0WjzfoVt3y1A+oxhcIAQUUGyNgNmebfEN07
JlX0B8UI8aIMjyfpsIDgZx1piHKwittGy/V4MPOLbmT2BBwS9BELcmWE4CwWbfeo
JM4lXY0hk3M/GTstRYaUKiwBWEU+z1PdAmWAgrtxpVnXN+oaToWh0WbVkbF1K6r5
3oBGFTL7kT+Wogg9GK04XoAREw0jZJ/k77oJx68lGVBNiB+Ue2KqX0H/0cf2CslY
Ug==
-----END CERTIFICATE-----
"/>
|
<meta-data android:name="expo.modules.updates.CODE_SIGNING_CERTIFICATE" android:value="-----BEGIN CERTIFICATE-----
MIICzTCCAbWgAwIBAgIJAPrPjxnUOGisMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV
BAMTBVNhdWNpMB4XDTI2MDgzMDE4MTAzMFoXDTM2MDgzMDE4MTAzMFowEDEOMAwG
A1UEAxMFU2F1Y2kwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWiUac
8i9bIbNZjv8iunVr6kqlgb9vea98JHz/A83biyszg0+GpmLYsr4zV2F7A6RitiIx
SOML+wVedyD0F2quWccBGXinvMAds0TxgIOjQfP+8+VJ/duDjveHvn+OFW4s7XxW
SY7TK1ufDcPWo/f5XzfqtBveug49zv3/F3MwfmMUbEY/zvH7nayPGJNuSyf3wISn
JIt0BsHNhWykRXCzjaF2fy3nAebOyezbtbapCjUlxPENVtBCzBQ7C9kaQXCBw5mV
hxhJlOxHWXpHJgDLHpev8A/CWQRh67tsevjKi6R6SDlf0R0xSbDJTElx8goWOOQW
VO9aDqI+oH0KpWfnAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E
DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEAyUc20bIRCxoBTJg84mPl
2nFDWSaAfzgo0K0b6x2T1suuq4LAOUgzBqJISrQAALd0besK1qUZSAMhjQu1wPZT
MHuWRMxQHfOkyHMYUfOoPMS4bE+4K0WjzfoVt3y1A+oxhcIAQUUGyNgNmebfEN07
JlX0B8UI8aIMjyfpsIDgZx1piHKwittGy/V4MPOLbmT2BBwS9BELcmWE4CwWbfeo
JM4lXY0hk3M/GTstRYaUKiwBWEU+z1PdAmWAgrtxpVnXN+oaToWh0WbVkbF1K6r5
3oBGFTL7kT+Wogg9GK04XoAREw0jZJ/k77oJx68lGVBNiB+Ue2KqX0H/0cf2CslY
Ug==
-----END CERTIFICATE-----
"/>
|
||||||
<meta-data android:name="expo.modules.updates.CODE_SIGNING_METADATA" android:value="{"keyid":"main","alg":"rsa-v1_5-sha256"}"/>
|
<meta-data android:name="expo.modules.updates.CODE_SIGNING_METADATA" android:value="{"keyid":"main","alg":"rsa-v1_5-sha256"}"/>
|
||||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
|
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
|
||||||
|
<meta-data android:name="expo.modules.updates.ENABLE_BSDIFF_PATCH_SUPPORT" android:value="true"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
|
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="5000"/>
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="5000"/>
|
||||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://ota.apps.jbrunton.co.uk/manifest"/>
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://ota.apps.jbrunton.co.uk/manifest"/>
|
||||||
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value="{"expo-channel-name":"production","expo-app-id":"9a66f3c2-fda7-416f-87e2-6db833b32e9d","xprem-branch":""}"/>
|
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value="{"expo-channel-name":"production","expo-app-id":"9a66f3c2-fda7-416f-87e2-6db833b32e9d","xprem-branch":""}"/>
|
||||||
<service android:name="com.reactnativeandroidwidget.RNWidgetCollectionService" android:permission="android.permission.BIND_REMOTEVIEWS"/>
|
<service android:name="com.reactnativeandroidwidget.RNWidgetCollectionService" android:permission="android.permission.BIND_REMOTEVIEWS"/>
|
||||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|smallestScreenSize|assetsPaths" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
@@ -46,7 +47,6 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<data android:scheme="app.sauci"/>
|
<data android:scheme="app.sauci"/>
|
||||||
<data android:scheme="com.sauci.app"/>
|
|
||||||
<data android:scheme="exp+sauci"/>
|
<data android:scheme="exp+sauci"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true" data-generated="true">
|
<intent-filter android:autoVerify="true" data-generated="true">
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<receiver android:name=".widget.LiveDraw" android:exported="true" android:label="Live Draw">
|
<receiver android:name=".widget.LiveDraw" android:exported="false" android:label="Live Draw">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
<action android:name="com.sauci.app.WIDGET_CLICK"/>
|
<action android:name="com.sauci.app.WIDGET_CLICK"/>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package com.sauci.app
|
package com.sauci.app
|
||||||
|
import expo.modules.splashscreen.SplashScreenManager
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -15,7 +16,10 @@ class MainActivity : ReactActivity() {
|
|||||||
// Set the theme to AppTheme BEFORE onCreate to support
|
// Set the theme to AppTheme BEFORE onCreate to support
|
||||||
// coloring the background, status bar, and navigation bar.
|
// coloring the background, status bar, and navigation bar.
|
||||||
// This is required for expo-splash-screen.
|
// This is required for expo-splash-screen.
|
||||||
setTheme(R.style.AppTheme);
|
// setTheme(R.style.AppTheme);
|
||||||
|
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||||
|
SplashScreenManager.registerOnActivity(this)
|
||||||
|
// @generated end expo-splashscreen
|
||||||
super.onCreate(null)
|
super.onCreate(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,40 +5,27 @@ import android.content.res.Configuration
|
|||||||
|
|
||||||
import com.facebook.react.PackageList
|
import com.facebook.react.PackageList
|
||||||
import com.facebook.react.ReactApplication
|
import com.facebook.react.ReactApplication
|
||||||
import com.facebook.react.ReactNativeHost
|
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||||
import com.facebook.react.ReactPackage
|
import com.facebook.react.ReactPackage
|
||||||
import com.facebook.react.ReactHost
|
import com.facebook.react.ReactHost
|
||||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
|
||||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
|
||||||
import com.facebook.react.common.ReleaseLevel
|
import com.facebook.react.common.ReleaseLevel
|
||||||
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||||
|
|
||||||
import expo.modules.ApplicationLifecycleDispatcher
|
import expo.modules.ApplicationLifecycleDispatcher
|
||||||
import expo.modules.ReactNativeHostWrapper
|
import expo.modules.ExpoReactHostFactory
|
||||||
|
|
||||||
class MainApplication : Application(), ReactApplication {
|
class MainApplication : Application(), ReactApplication {
|
||||||
|
|
||||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
override val reactHost: ReactHost by lazy {
|
||||||
this,
|
ExpoReactHostFactory.getDefaultReactHost(
|
||||||
object : DefaultReactNativeHost(this) {
|
context = applicationContext,
|
||||||
override fun getPackages(): List<ReactPackage> {
|
packageList =
|
||||||
val packages = PackageList(this).packages
|
PackageList(this).packages.apply {
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||||
// packages.add(new MyReactNativePackage());
|
// add(MyReactNativePackage())
|
||||||
return packages
|
}
|
||||||
}
|
)
|
||||||
|
}
|
||||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
|
||||||
|
|
||||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
|
||||||
|
|
||||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
|
||||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
override val reactHost: ReactHost
|
|
||||||
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|||||||
@@ -1,84 +1,6 @@
|
|||||||
package com.sauci.app.widget;
|
package com.sauci.app.widget;
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager;
|
import com.reactnativeandroidwidget.RNWidgetProvider;
|
||||||
import android.appwidget.AppWidgetProvider;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.RectF;
|
|
||||||
import android.util.Base64;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.widget.RemoteViews;
|
|
||||||
|
|
||||||
import com.sauci.app.R;
|
public class LiveDraw extends RNWidgetProvider {
|
||||||
|
|
||||||
public class LiveDraw extends AppWidgetProvider {
|
|
||||||
|
|
||||||
private static final String PREFS_NAME_SUFFIX = ".widgetdata";
|
|
||||||
private static final String KEY_IMAGE = "LiveDrawImage";
|
|
||||||
private static final int BG_COLOR = 0xFF1A1A2E;
|
|
||||||
private static final int CORNER_RADIUS_DP = 16;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
|
||||||
for (int widgetId : appWidgetIds) {
|
|
||||||
updateWidget(context, appWidgetManager, widgetId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateWidget(Context context, AppWidgetManager appWidgetManager, int widgetId) {
|
|
||||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_live_draw);
|
|
||||||
|
|
||||||
String base64 = getStoredImage(context);
|
|
||||||
if (base64 != null && !base64.isEmpty()) {
|
|
||||||
try {
|
|
||||||
byte[] bytes = Base64.decode(base64, Base64.DEFAULT);
|
|
||||||
Bitmap decoded = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
|
||||||
if (decoded != null) {
|
|
||||||
Bitmap rounded = addRoundedBackground(context, decoded);
|
|
||||||
views.setImageViewBitmap(R.id.widget_image, rounded);
|
|
||||||
decoded.recycle();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
setPlaceholder(views);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setPlaceholder(views);
|
|
||||||
}
|
|
||||||
|
|
||||||
appWidgetManager.updateAppWidget(widgetId, views);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getStoredImage(Context context) {
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(
|
|
||||||
context.getPackageName() + PREFS_NAME_SUFFIX, Context.MODE_PRIVATE);
|
|
||||||
return prefs.getString(KEY_IMAGE, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPlaceholder(RemoteViews views) {
|
|
||||||
views.setImageViewResource(R.id.widget_image, R.drawable.widget_placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap addRoundedBackground(Context context, Bitmap drawing) {
|
|
||||||
float radius = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, CORNER_RADIUS_DP,
|
|
||||||
context.getResources().getDisplayMetrics());
|
|
||||||
|
|
||||||
int w = drawing.getWidth();
|
|
||||||
int h = drawing.getHeight();
|
|
||||||
|
|
||||||
Bitmap output = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
|
||||||
Canvas canvas = new Canvas(output);
|
|
||||||
|
|
||||||
Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
||||||
bgPaint.setColor(BG_COLOR);
|
|
||||||
canvas.drawRoundRect(new RectF(0, 0, w, h), radius, radius, bgPaint);
|
|
||||||
|
|
||||||
canvas.drawBitmap(drawing, 0, 0, null);
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 24 KiB |
@@ -2,6 +2,5 @@
|
|||||||
<color name="splashscreen_background">#1a1a2e</color>
|
<color name="splashscreen_background">#1a1a2e</color>
|
||||||
<color name="iconBackground">#1a1a2e</color>
|
<color name="iconBackground">#1a1a2e</color>
|
||||||
<color name="colorPrimary">#023c69</color>
|
<color name="colorPrimary">#023c69</color>
|
||||||
<color name="colorPrimaryDark">#1a1a2e</color>
|
|
||||||
<color name="notification_icon_color">#e94560</color>
|
<color name="notification_icon_color">#e94560</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Sauci</string>
|
<string name="app_name">Sauci</string>
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_runtime_version">1.0.8</string>
|
||||||
<string name="widget_livedraw_description" translatable="false">See your shared drawing with your partner.</string>
|
<string name="widget_livedraw_description" translatable="false">See your shared drawing with your partner.</string>
|
||||||
<string name="expo_runtime_version">1.0.7</string>
|
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">true</item>
|
|
||||||
<item name="android:textColor">@android:color/black</item>
|
|
||||||
<item name="android:editTextStyle">@style/ResetEditText</item>
|
|
||||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="android:statusBarColor">#1a1a2e</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="ResetEditText" parent="@android:style/Widget.EditText">
|
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||||
<item name="android:padding">0dp</item>
|
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||||
<item name="android:textColorHint">#c8c8c8</item>
|
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
||||||
<item name="android:textColor">@android:color/black</item>
|
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||||
</style>
|
<item name="android:windowSplashScreenBehavior">icon_preferred</item>
|
||||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
|
||||||
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -4,10 +4,18 @@
|
|||||||
android:minHeight="110dp"
|
android:minHeight="110dp"
|
||||||
android:targetCellWidth="4"
|
android:targetCellWidth="4"
|
||||||
android:targetCellHeight="2"
|
android:targetCellHeight="2"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
android:resizeMode="none"
|
android:resizeMode="none"
|
||||||
|
|
||||||
android:description="@string/widget_livedraw_description"
|
android:description="@string/widget_livedraw_description"
|
||||||
android:initialLayout="@layout/widget_live_draw"
|
|
||||||
|
android:initialLayout="@layout/rn_widget"
|
||||||
android:previewImage="@drawable/livedraw_preview"
|
android:previewImage="@drawable/livedraw_preview"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
android:updatePeriodMillis="1800000"
|
android:updatePeriodMillis="1800000"
|
||||||
android:widgetCategory="home_screen">
|
android:widgetCategory="home_screen">
|
||||||
</appwidget-provider>
|
</appwidget-provider>
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6'
|
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6'
|
||||||
classpath 'com.google.gms:google-services:4.4.1'
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
classpath('com.android.tools.build:gradle')
|
classpath('com.android.tools.build:gradle')
|
||||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://www.jitpack.io' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: "expo-root-project"
|
apply plugin: "expo-root-project"
|
||||||
apply plugin: "com.facebook.react.rootproject"
|
apply plugin: "com.facebook.react.rootproject"
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
maven { url 'https://www.jitpack.io' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
|
||||||
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
# Android operating system, and which are packaged with your app's APK
|
# Android operating system, and which are packaged with your app's APK
|
||||||
@@ -41,6 +41,11 @@ newArchEnabled=true
|
|||||||
# If set to false, you will be using JSC instead.
|
# If set to false, you will be using JSC instead.
|
||||||
hermesEnabled=true
|
hermesEnabled=true
|
||||||
|
|
||||||
|
# Use this property to enable edge-to-edge display support.
|
||||||
|
# This allows your app to draw behind system bars for an immersive UI.
|
||||||
|
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||||
|
edgeToEdgeEnabled=true
|
||||||
|
|
||||||
# Enable GIF support in React Native images (~200 B increase)
|
# Enable GIF support in React Native images (~200 B increase)
|
||||||
expo.gif.enabled=true
|
expo.gif.enabled=true
|
||||||
# Enable webp support in React Native images (~85 KB increase)
|
# Enable webp support in React Native images (~85 KB increase)
|
||||||
@@ -55,16 +60,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
|||||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||||
expo.useLegacyPackaging=false
|
expo.useLegacyPackaging=false
|
||||||
|
|
||||||
android.targetSdkVersion=36
|
expo.inlineModules.watchedDirectories=[]
|
||||||
android.extraMavenRepos=[]
|
|
||||||
# Specifies whether the app is configured to use edge-to-edge via the app config or plugin
|
|
||||||
# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge.
|
|
||||||
expo.edgeToEdgeEnabled=true
|
|
||||||
|
|
||||||
# Use this property to enable edge-to-edge display support.
|
|
||||||
# This allows your app to draw behind system bars for an immersive UI.
|
|
||||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
|
||||||
edgeToEdgeEnabled=true
|
|
||||||
|
|
||||||
android.compileSdkVersion=36
|
android.compileSdkVersion=36
|
||||||
android.buildToolsVersion=36.0.0
|
android.targetSdkVersion=36
|
||||||
|
android.buildToolsVersion=36.0.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -115,7 +114,6 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -173,7 +171,6 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -206,15 +203,14 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
@REM
|
||||||
|
@REM This source code is licensed under the MIT license found in the
|
||||||
|
@REM LICENSE file in the root directory of this source tree.
|
||||||
|
|
||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@@ -70,11 +75,10 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pluginManagement {
|
|||||||
}.standardOutput.asText.get().trim()
|
}.standardOutput.asText.get().trim()
|
||||||
).getParentFile().absolutePath
|
).getParentFile().absolutePath
|
||||||
includeBuild(reactNativeGradlePlugin)
|
includeBuild(reactNativeGradlePlugin)
|
||||||
|
|
||||||
def expoPluginsPath = new File(
|
def expoPluginsPath = new File(
|
||||||
providers.exec {
|
providers.exec {
|
||||||
workingDir(rootDir)
|
workingDir(rootDir)
|
||||||
@@ -29,10 +29,10 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
|||||||
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
expoAutolinking.useExpoModules()
|
||||||
|
|
||||||
rootProject.name = 'Sauci'
|
rootProject.name = 'Sauci'
|
||||||
|
|
||||||
expoAutolinking.useExpoModules()
|
|
||||||
expoAutolinking.useExpoVersionCatalog()
|
expoAutolinking.useExpoVersionCatalog()
|
||||||
|
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ module.exports = () => ({
|
|||||||
// Keep this explicit because EOAS validates a primitive runtime string before
|
// Keep this explicit because EOAS validates a primitive runtime string before
|
||||||
// upload. The release version script updates it atomically with app.json and
|
// upload. The release version script updates it atomically with app.json and
|
||||||
// the checked-in native runtime values.
|
// the checked-in native runtime values.
|
||||||
runtimeVersion: '1.0.7',
|
runtimeVersion: '1.0.8',
|
||||||
updates: {
|
updates: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url: OTA_MANIFEST_URL,
|
url: OTA_MANIFEST_URL,
|
||||||
|
|||||||
@@ -2,17 +2,15 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Sauci",
|
"name": "Sauci",
|
||||||
"slug": "sauci",
|
"slug": "sauci",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"scheme": "app.sauci",
|
"scheme": "app.sauci",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"platforms": [
|
||||||
"splash": {
|
"ios",
|
||||||
"image": "./assets/splash-icon.png",
|
"android"
|
||||||
"resizeMode": "contain",
|
],
|
||||||
"backgroundColor": "#1a1a2e"
|
|
||||||
},
|
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": false,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.sauci.app",
|
"bundleIdentifier": "com.sauci.app",
|
||||||
@@ -27,7 +25,7 @@
|
|||||||
"group.com.sauci.app"
|
"group.com.sauci.app"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"buildNumber": "46",
|
"buildNumber": "49",
|
||||||
"usesAppleSignIn": true,
|
"usesAppleSignIn": true,
|
||||||
"googleServicesFile": "./GoogleService-Info.plist",
|
"googleServicesFile": "./GoogleService-Info.plist",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
@@ -189,7 +187,7 @@
|
|||||||
"backgroundColor": "#1a1a2e"
|
"backgroundColor": "#1a1a2e"
|
||||||
},
|
},
|
||||||
"package": "com.sauci.app",
|
"package": "com.sauci.app",
|
||||||
"versionCode": 49,
|
"versionCode": 51,
|
||||||
"googleServicesFile": "./google-services.json",
|
"googleServicesFile": "./google-services.json",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"RECEIVE_BOOT_COMPLETED",
|
"RECEIVE_BOOT_COMPLETED",
|
||||||
@@ -223,6 +221,14 @@
|
|||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"./plugins/withFixHoisting.js",
|
"./plugins/withFixHoisting.js",
|
||||||
|
[
|
||||||
|
"expo-splash-screen",
|
||||||
|
{
|
||||||
|
"image": "./assets/splash-icon.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#1a1a2e"
|
||||||
|
}
|
||||||
|
],
|
||||||
"@react-native-firebase/app",
|
"@react-native-firebase/app",
|
||||||
[
|
[
|
||||||
"expo-build-properties",
|
"expo-build-properties",
|
||||||
@@ -320,7 +326,10 @@
|
|||||||
"origin": false
|
"origin": false
|
||||||
},
|
},
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "807ca0af-e9bd-40e8-a658-f3382072c724"
|
"projectId": "807ca0af-e9bd-40e8-a658-f3382072c724",
|
||||||
|
"observe": {
|
||||||
|
"endpointUrl": "https://ota.apps.jbrunton.co.uk/observe/9a66f3c2-fda7-416f-87e2-6db833b32e9d"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": "jjbrunton-apps"
|
"owner": "jjbrunton-apps"
|
||||||
|
|||||||
@@ -69,10 +69,12 @@ export default function MatchesScreen() {
|
|||||||
checkNudgeCooldown,
|
checkNudgeCooldown,
|
||||||
} = useMatchStore();
|
} = useMatchStore();
|
||||||
const { user, couple, partner } = useAuthStore();
|
const { user, couple, partner } = useAuthStore();
|
||||||
|
const userId = user?.id;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
||||||
const [showTutorial, setShowTutorial] = useState(false);
|
const [showTutorial, setShowTutorial] = useState(false);
|
||||||
|
const tutorialDismissedRef = useRef(false);
|
||||||
const [nudgeFeedback, setNudgeFeedback] = useState<string | null>(null);
|
const [nudgeFeedback, setNudgeFeedback] = useState<string | null>(null);
|
||||||
const [headerHeight, setHeaderHeight] = useState(130);
|
const [headerHeight, setHeaderHeight] = useState(130);
|
||||||
|
|
||||||
@@ -104,21 +106,30 @@ export default function MatchesScreen() {
|
|||||||
// Check if tutorial should be shown when screen is focused or matches change
|
// Check if tutorial should be shown when screen is focused or matches change
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
let isActive = true;
|
||||||
|
|
||||||
const checkTutorial = async () => {
|
const checkTutorial = async () => {
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0 && userId) {
|
||||||
const seen = await hasSeenMatchesTutorial();
|
const seen = await hasSeenMatchesTutorial(userId);
|
||||||
if (!seen) {
|
if (isActive && !tutorialDismissedRef.current && !seen) {
|
||||||
setShowTutorial(true);
|
setShowTutorial(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
checkTutorial();
|
void checkTutorial();
|
||||||
}, [matches.length])
|
|
||||||
|
return () => {
|
||||||
|
isActive = false;
|
||||||
|
};
|
||||||
|
}, [matches.length, userId])
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTutorialComplete = async () => {
|
const handleTutorialComplete = async () => {
|
||||||
await markMatchesTutorialSeen();
|
if (!userId) return;
|
||||||
|
|
||||||
|
tutorialDismissedRef.current = true;
|
||||||
setShowTutorial(false);
|
setShowTutorial(false);
|
||||||
|
await markMatchesTutorialSeen(userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load whichever view is showing, once. Returning to this tab reuses what is
|
// Load whichever view is showing, once. Returning to this tab reuses what is
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ const styles = StyleSheet.create({
|
|||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
},
|
},
|
||||||
navBarBackground: {
|
navBarBackground: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.border,
|
borderBottomColor: colors.border,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Image,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeInUp,
|
|
||||||
FadeOut,
|
FadeOut,
|
||||||
SlideInRight,
|
SlideInRight,
|
||||||
SlideOutLeft,
|
SlideOutLeft,
|
||||||
@@ -32,9 +30,9 @@ import { getProfileError } from '../../src/lib/errors';
|
|||||||
import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications';
|
import { registerForPushNotificationsAsync, savePushToken } from '../../src/lib/notifications';
|
||||||
import { Events } from '../../src/lib/analytics';
|
import { Events } from '../../src/lib/analytics';
|
||||||
import { hasSeenOnboardingPaywall, markOnboardingPaywallSeen } from '../../src/lib/onboardingPaywallSeen';
|
import { hasSeenOnboardingPaywall, markOnboardingPaywallSeen } from '../../src/lib/onboardingPaywallSeen';
|
||||||
import { useAvatarPicker } from '../../src/hooks/useAvatarPicker';
|
import { REQUIRED_ONBOARDING_VERSION, ONBOARDING_OFFERING_ID } from '../../src/constants/onboarding';
|
||||||
import { REQUIRED_ONBOARDING_VERSION, ONBOARDING_OFFERING_ID } from '../../src/constants/onboarding';
|
import { activeOnboardingStage, initialOnboardingStage, previousOnboardingStage, type OnboardingStage } from '../../src/lib/onboardingStages';
|
||||||
import type { Gender, UsageReason } from '../../src/types';
|
import type { Gender, UsageReason } from '../../src/types';
|
||||||
|
|
||||||
const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
|
const GENDER_OPTIONS: { value: Gender; label: string; icon: string }[] = [
|
||||||
{ value: 'male', label: 'Male', icon: 'male' },
|
{ value: 'male', label: 'Male', icon: 'male' },
|
||||||
@@ -51,15 +49,17 @@ const PURPOSE_OPTIONS: { value: UsageReason; label: string; icon: string }[] = [
|
|||||||
{ value: 'strengthen_relationship', label: 'Strengthen our relationship', icon: 'shield-checkmark' },
|
{ value: 'strengthen_relationship', label: 'Strengthen our relationship', icon: 'shield-checkmark' },
|
||||||
];
|
];
|
||||||
|
|
||||||
type Stage = 'avatar' | 'name' | 'gender' | 'purpose' | 'notifications';
|
type Stage = OnboardingStage;
|
||||||
|
|
||||||
export default function OnboardingScreen() {
|
export default function OnboardingScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// Only subscribe to stable values to prevent re-renders from background fetchUser calls
|
// Only subscribe to stable values to prevent re-renders from background fetchUser calls
|
||||||
const userId = useAuthStore((s) => s.user?.id);
|
const userId = useAuthStore((s) => s.user?.id);
|
||||||
|
const persistedUserName = useAuthStore((s) => s.user?.name);
|
||||||
|
const pendingAppleDisplayName = useAuthStore((s) => s.pendingAppleDisplayName);
|
||||||
const fetchUser = useAuthStore((s) => s.fetchUser);
|
const fetchUser = useAuthStore((s) => s.fetchUser);
|
||||||
const initialUser = useRef(useAuthStore.getState().user);
|
const initialUser = useRef(useAuthStore.getState().user);
|
||||||
const [stage, setStage] = useState<Stage>('avatar');
|
const [stage, setStage] = useState<Stage>(() => initialOnboardingStage(initialUser.current?.name));
|
||||||
const [name, setName] = useState(initialUser.current?.name || '');
|
const [name, setName] = useState(initialUser.current?.name || '');
|
||||||
const [gender, setGender] = useState<Gender | null>(initialUser.current?.gender || null);
|
const [gender, setGender] = useState<Gender | null>(initialUser.current?.gender || null);
|
||||||
const [usageReason, setUsageReason] = useState<UsageReason | null>(null);
|
const [usageReason, setUsageReason] = useState<UsageReason | null>(null);
|
||||||
@@ -67,38 +67,18 @@ export default function OnboardingScreen() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showPaywall, setShowPaywall] = useState(false);
|
const [showPaywall, setShowPaywall] = useState(false);
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
const paywallNavigationRef = useRef(false);
|
const paywallNavigationRef = useRef(false);
|
||||||
|
|
||||||
const {
|
const pendingDisplayName = pendingAppleDisplayName?.userId === userId ? pendingAppleDisplayName?.name : undefined;
|
||||||
avatarUri,
|
const displayName = persistedUserName?.trim() || pendingDisplayName || name;
|
||||||
isUploading: isUploadingAvatar,
|
const hasStoredDisplayName = Boolean(persistedUserName?.trim() || pendingDisplayName);
|
||||||
showPicker: handleAvatarPress,
|
const activeStage = activeOnboardingStage(stage, persistedUserName, pendingDisplayName);
|
||||||
uploadAvatar,
|
const previousStage = previousOnboardingStage(activeStage, hasStoredDisplayName);
|
||||||
} = useAvatarPicker({
|
|
||||||
userId,
|
|
||||||
onSelect: () => setError(null),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Events.onboardingStart();
|
Events.onboardingStart();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAvatarContinue = () => {
|
|
||||||
if (!avatarUri) {
|
|
||||||
setError('Please add a profile photo to continue');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setStage('name');
|
|
||||||
Events.onboardingStageComplete('avatar');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAvatarSkip = () => {
|
|
||||||
setError(null);
|
|
||||||
setStage('name');
|
|
||||||
Events.avatarSkipped();
|
|
||||||
Events.onboardingStageComplete('avatar');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNameSubmit = () => {
|
const handleNameSubmit = () => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
setError('Please enter your name');
|
setError('Please enter your name');
|
||||||
@@ -154,7 +134,7 @@ export default function OnboardingScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
console.log('[Onboarding] handleComplete called, user:', userId, 'name:', name);
|
console.log('[Onboarding] handleComplete called, user:', userId, 'name:', displayName);
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
console.error('[Onboarding] No user ID available');
|
console.error('[Onboarding] No user ID available');
|
||||||
setError('Not logged in. Please restart the app.');
|
setError('Not logged in. Please restart the app.');
|
||||||
@@ -165,19 +145,10 @@ export default function OnboardingScreen() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload avatar if one was selected
|
|
||||||
let avatarUrl: string | null = null;
|
|
||||||
if (avatarUri) {
|
|
||||||
avatarUrl = await uploadAvatar();
|
|
||||||
if (avatarUrl) {
|
|
||||||
Events.avatarUploaded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedFields: string[] = ["name", "gender", "usage_reason", "max_intensity", "show_explicit_content", "hide_nsfw"];
|
const updatedFields: string[] = ["name", "gender", "usage_reason", "max_intensity", "show_explicit_content", "hide_nsfw"];
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: name.trim(),
|
name: displayName.trim(),
|
||||||
gender,
|
gender,
|
||||||
usage_reason: usageReason,
|
usage_reason: usageReason,
|
||||||
// Preserve the safest legacy profile values. Catalogue
|
// Preserve the safest legacy profile values. Catalogue
|
||||||
@@ -191,10 +162,6 @@ export default function OnboardingScreen() {
|
|||||||
|
|
||||||
await profileSettingsApi.updateProfile(updateData);
|
await profileSettingsApi.updateProfile(updateData);
|
||||||
|
|
||||||
if (avatarUrl) {
|
|
||||||
updatedFields.push("avatar_url");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[Onboarding] Profile updated successfully, fetching user...');
|
console.log('[Onboarding] Profile updated successfully, fetching user...');
|
||||||
await fetchUser();
|
await fetchUser();
|
||||||
console.log('[Onboarding] User fetched, navigating to home...');
|
console.log('[Onboarding] User fetched, navigating to home...');
|
||||||
@@ -231,72 +198,8 @@ export default function OnboardingScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStage = () => {
|
const renderStage = () => {
|
||||||
switch (stage) {
|
switch (activeStage) {
|
||||||
case 'avatar':
|
case 'name':
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
key="avatar"
|
|
||||||
entering={FadeInUp.duration(500)}
|
|
||||||
exiting={SlideOutLeft.duration(300)}
|
|
||||||
style={styles.stageContainer}
|
|
||||||
>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={gradients.primary as [string, string]}
|
|
||||||
style={styles.iconContainer}
|
|
||||||
>
|
|
||||||
<Ionicons name="heart" size={40} color={colors.text} />
|
|
||||||
</LinearGradient>
|
|
||||||
<Text style={styles.title}>Welcome to Sauci</Text>
|
|
||||||
<Text style={styles.subtitle}>
|
|
||||||
Add a profile photo so your partner knows it's you
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<GlassCard style={styles.card}>
|
|
||||||
<View style={styles.avatarSection}>
|
|
||||||
<Pressable onPress={handleAvatarPress} style={styles.avatarTouchable}>
|
|
||||||
{avatarUri ? (
|
|
||||||
<Image source={{ uri: avatarUri }} style={styles.avatarImage} />
|
|
||||||
) : (
|
|
||||||
<View
|
|
||||||
style={styles.avatarPlaceholder}
|
|
||||||
>
|
|
||||||
<Ionicons name="camera" size={40} color={colors.textSecondary} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={styles.avatarBadge}>
|
|
||||||
<Ionicons name={avatarUri ? "pencil" : "add"} size={16} color={colors.text} />
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
<Text style={styles.avatarHint}>
|
|
||||||
{avatarUri ? 'Tap to change photo' : 'Tap to add a photo'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{error && (
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<Ionicons name="alert-circle" size={16} color={colors.error} />
|
|
||||||
<Text style={styles.errorText}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</GlassCard>
|
|
||||||
|
|
||||||
<View style={styles.footer}>
|
|
||||||
<GlassButton onPress={handleAvatarContinue} fullWidth size="lg">
|
|
||||||
Continue
|
|
||||||
</GlassButton>
|
|
||||||
<Pressable
|
|
||||||
style={styles.skipButton}
|
|
||||||
onPress={handleAvatarSkip}
|
|
||||||
testID="avatar-skip-button"
|
|
||||||
>
|
|
||||||
<Text style={styles.skipText}>Skip for now</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'name':
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
key="name"
|
key="name"
|
||||||
@@ -305,16 +208,12 @@ export default function OnboardingScreen() {
|
|||||||
style={styles.stageContainer}
|
style={styles.stageContainer}
|
||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
{avatarUri ? (
|
<LinearGradient
|
||||||
<Image source={{ uri: avatarUri }} style={styles.headerAvatar} />
|
colors={gradients.primary as [string, string]}
|
||||||
) : (
|
style={styles.iconContainer}
|
||||||
<LinearGradient
|
>
|
||||||
colors={gradients.primary as [string, string]}
|
<Ionicons name="person" size={40} color={colors.text} />
|
||||||
style={styles.iconContainer}
|
</LinearGradient>
|
||||||
>
|
|
||||||
<Ionicons name="person" size={40} color={colors.text} />
|
|
||||||
</LinearGradient>
|
|
||||||
)}
|
|
||||||
<Text style={styles.title}>What can we call you?</Text>
|
<Text style={styles.title}>What can we call you?</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Nicknames and pet names are welcome too!
|
Nicknames and pet names are welcome too!
|
||||||
@@ -368,7 +267,7 @@ export default function OnboardingScreen() {
|
|||||||
style={styles.stageContainer}
|
style={styles.stageContainer}
|
||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.greeting}>Hey, {name.trim()}!</Text>
|
<Text style={styles.greeting}>Hey, {displayName.trim()}!</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
This helps us show you the right questions
|
This helps us show you the right questions
|
||||||
</Text>
|
</Text>
|
||||||
@@ -576,28 +475,24 @@ export default function OnboardingScreen() {
|
|||||||
>
|
>
|
||||||
{/* Header with back button and progress dots */}
|
{/* Header with back button and progress dots */}
|
||||||
<Animated.View entering={FadeIn.duration(600)} style={styles.headerBar}>
|
<Animated.View entering={FadeIn.duration(600)} style={styles.headerBar}>
|
||||||
{stage !== 'avatar' ? (
|
{previousStage ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
if (stage === 'notifications') setStage('purpose');
|
setStage(previousStage);
|
||||||
else if (stage === 'purpose') setStage('gender');
|
}}
|
||||||
else if (stage === 'gender') setStage('name');
|
|
||||||
else if (stage === 'name') setStage('avatar');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.backButtonPlaceholder} />
|
<View style={styles.backButtonPlaceholder} />
|
||||||
)}
|
)}
|
||||||
<View style={styles.progressContainer}>
|
<View style={styles.progressContainer}>
|
||||||
<View style={[styles.progressDot, stage === 'avatar' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, activeStage === 'name' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'name' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, activeStage === 'gender' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'gender' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, activeStage === 'purpose' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'purpose' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, activeStage === 'notifications' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, stage === 'notifications' && styles.progressDotActive]} />
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.backButtonPlaceholder} />
|
<View style={styles.backButtonPlaceholder} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -653,18 +548,6 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: colors.border,
|
backgroundColor: colors.border,
|
||||||
},
|
},
|
||||||
// ...
|
// ...
|
||||||
avatarPlaceholder: {
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.border,
|
|
||||||
borderStyle: 'dashed',
|
|
||||||
backgroundColor: colors.backgroundLight,
|
|
||||||
},
|
|
||||||
// ...
|
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -757,47 +640,6 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: spacing.lg,
|
marginBottom: spacing.lg,
|
||||||
...shadows.lg,
|
...shadows.lg,
|
||||||
},
|
},
|
||||||
headerAvatar: {
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 40,
|
|
||||||
marginBottom: spacing.lg,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.primary,
|
|
||||||
},
|
|
||||||
avatarSection: {
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: spacing.lg,
|
|
||||||
},
|
|
||||||
avatarTouchable: {
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
avatarImage: {
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
borderWidth: 3,
|
|
||||||
borderColor: colors.primary,
|
|
||||||
},
|
|
||||||
|
|
||||||
avatarBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 4,
|
|
||||||
right: 4,
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 16,
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: colors.background,
|
|
||||||
},
|
|
||||||
avatarHint: {
|
|
||||||
...typography.caption1,
|
|
||||||
color: colors.textSecondary,
|
|
||||||
marginTop: spacing.md,
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
...typography.largeTitle,
|
...typography.largeTitle,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ if (Platform.OS === "android") {
|
|||||||
import { authClient } from "../../src/lib/authClient";
|
import { authClient } from "../../src/lib/authClient";
|
||||||
import { getAuthError } from "../../src/lib/errors";
|
import { getAuthError } from "../../src/lib/errors";
|
||||||
import { Events } from "../../src/lib/analytics";
|
import { Events } from "../../src/lib/analytics";
|
||||||
|
import { apiRequestWithAccessToken } from "../../src/lib/apiClient";
|
||||||
|
import { formatAppleProfileName } from "../../src/lib/appleProfileName";
|
||||||
|
import { persistAppleProfileName } from "../../src/lib/appleProfileNamePersistence";
|
||||||
|
import { profileSettingsApi } from "../../src/lib/profileSettingsApi";
|
||||||
|
import type { Profile } from "../../src/types";
|
||||||
import { useAuthStore } from "../../src/store";
|
import { useAuthStore } from "../../src/store";
|
||||||
import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui";
|
import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui";
|
||||||
import { colors, spacing, radius, typography } from "../../src/theme";
|
import { colors, spacing, radius, typography } from "../../src/theme";
|
||||||
@@ -290,7 +295,7 @@ export default function LoginScreen() {
|
|||||||
console.log("[Apple Sign In] Got credential:", !!credential, "identityToken:", !!credential?.identityToken);
|
console.log("[Apple Sign In] Got credential:", !!credential, "identityToken:", !!credential?.identityToken);
|
||||||
|
|
||||||
if (credential.identityToken) {
|
if (credential.identityToken) {
|
||||||
const { error } = await authClient.auth.signInWithIdToken({
|
const { data, error } = await authClient.auth.signInWithIdToken({
|
||||||
provider: "apple",
|
provider: "apple",
|
||||||
token: credential.identityToken,
|
token: credential.identityToken,
|
||||||
});
|
});
|
||||||
@@ -299,6 +304,36 @@ export default function LoginScreen() {
|
|||||||
console.log("[Apple Sign In] Supabase error:", error.message);
|
console.log("[Apple Sign In] Supabase error:", error.message);
|
||||||
Alert.alert("Error", getAuthError(error));
|
Alert.alert("Error", getAuthError(error));
|
||||||
} else {
|
} else {
|
||||||
|
const appleProfileName = formatAppleProfileName(credential.fullName);
|
||||||
|
const userId = data.user?.id;
|
||||||
|
const accessToken = data.session?.access_token;
|
||||||
|
if (appleProfileName && userId && accessToken) {
|
||||||
|
const authStore = useAuthStore.getState();
|
||||||
|
authStore.setPendingAppleDisplayName({ userId, name: appleProfileName });
|
||||||
|
try {
|
||||||
|
await persistAppleProfileName(
|
||||||
|
{ userId, accessToken },
|
||||||
|
appleProfileName,
|
||||||
|
{
|
||||||
|
getProfile: (token) => apiRequestWithAccessToken<{ profile: Profile }>("/v1/me", token),
|
||||||
|
updateProfileName: (token, name) => profileSettingsApi.updateProfileWithAccessToken(token, { name }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await authStore.fetchUser({
|
||||||
|
userId,
|
||||||
|
accessToken,
|
||||||
|
isAnonymous: !!(data.user as { is_anonymous?: boolean } | null)?.is_anonymous,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// The account session remains valid. Clear the subject-keyed
|
||||||
|
// temporary name so onboarding safely falls back to its name step.
|
||||||
|
console.warn("[Apple Sign In] Could not persist supplied name");
|
||||||
|
} finally {
|
||||||
|
useAuthStore.getState().clearPendingAppleDisplayName(userId);
|
||||||
|
}
|
||||||
|
} else if (appleProfileName) {
|
||||||
|
Alert.alert("Error", "Apple sign in did not return an authenticated session. Please try again.");
|
||||||
|
}
|
||||||
Events.signIn("apple");
|
Events.signIn("apple");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import "../src/polyfills/web";
|
|||||||
import { initCrashlytics, setUserContext, clearUserContext } from "../src/lib/crashlytics";
|
import { initCrashlytics, setUserContext, clearUserContext } from "../src/lib/crashlytics";
|
||||||
initCrashlytics();
|
initCrashlytics();
|
||||||
|
|
||||||
|
// Install the Observe global JS error handler after Crashlytics so it wraps
|
||||||
|
// (and always calls) whatever handler is already registered.
|
||||||
|
import { installObserveGlobalErrorHandler, logAppStarted } from "../src/lib/observe";
|
||||||
|
installObserveGlobalErrorHandler();
|
||||||
|
|
||||||
// Analytics imports (initialization deferred until app mounts)
|
// Analytics imports (initialization deferred until app mounts)
|
||||||
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
|
import { initAnalytics, setUserId, clearUserId, logScreenView, Events } from "../src/lib/analytics";
|
||||||
|
|
||||||
@@ -73,6 +78,7 @@ export default function RootLayout() {
|
|||||||
analyticsInitialized.current = true;
|
analyticsInitialized.current = true;
|
||||||
initAnalytics();
|
initAnalytics();
|
||||||
Events.appOpened("cold");
|
Events.appOpened("cold");
|
||||||
|
logAppStarted();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -115,15 +121,15 @@ export default function RootLayout() {
|
|||||||
// Fetch user on mount
|
// Fetch user on mount
|
||||||
fetchUser();
|
fetchUser();
|
||||||
|
|
||||||
// Handle initial URL if app was opened via deep link
|
// Handle initial URL if app was opened via deep link
|
||||||
const handleDeepLink = async (url: string | null) => {
|
const handleDeepLink = async (url: string | null) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
// Invite links (https://sauci.app/join/{code} or app.sauci://join?code={code})
|
// Invite links (https://sauci.app/join/{code} or app.sauci://join?code={code})
|
||||||
// are handled separately from the Supabase auth deep link flow below.
|
// are handled separately from the Supabase auth deep link flow below.
|
||||||
const inviteLink = parseInviteLinkCode(url);
|
const inviteLink = parseInviteLinkCode(url);
|
||||||
if (inviteLink) {
|
if (inviteLink) {
|
||||||
Events.inviteLinkOpened(inviteLink.source);
|
Events.inviteLinkOpened(inviteLink.source);
|
||||||
await stashPendingInviteCode(inviteLink.code);
|
await stashPendingInviteCode(inviteLink.code);
|
||||||
|
|
||||||
const { isAuthenticated, user } = useAuthStore.getState();
|
const { isAuthenticated, user } = useAuthStore.getState();
|
||||||
@@ -200,15 +206,15 @@ export default function RootLayout() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get initial URL
|
// Get initial URL
|
||||||
Linking.getInitialURL().then((url) => {
|
Linking.getInitialURL().then((url) => {
|
||||||
handleDeepLink(url);
|
handleDeepLink(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for new deep links while app is open
|
// Listen for new deep links while app is open
|
||||||
const linkingSubscription = Linking.addEventListener("url", (event) => {
|
const linkingSubscription = Linking.addEventListener("url", (event) => {
|
||||||
handleDeepLink(event.url);
|
handleDeepLink(event.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up push notification response listener (for taps while app is running)
|
// Set up push notification response listener (for taps while app is running)
|
||||||
const notificationSubscription = setupNotificationResponseListener();
|
const notificationSubscription = setupNotificationResponseListener();
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ const styles = StyleSheet.create({
|
|||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
},
|
},
|
||||||
navBarBackground: {
|
navBarBackground: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.border,
|
borderBottomColor: colors.border,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
json_key_file(ENV["GOOGLE_PLAY_KEY_FILE"] || File.expand_path("../fastlane-key.json", __dir__))
|
json_key_file(ENV["GOOGLE_PLAY_KEY_FILE"] || File.expand_path("../fastlane-key.json", __dir__))
|
||||||
package_name("com.sauci.app")
|
package_name("com.sauci.app")
|
||||||
|
app_identifier("com.sauci.app")
|
||||||
@@ -45,4 +45,86 @@ platform :android do
|
|||||||
lane :upload_beta do |options|
|
lane :upload_beta do |options|
|
||||||
upload(options.merge(track: "beta"))
|
upload(options.merge(track: "beta"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Upload AAB and release it: commits the release on the given track (default production), triggering Play review and rollout on approval"
|
||||||
|
lane :release do |options|
|
||||||
|
track = options[:track] || "production"
|
||||||
|
|
||||||
|
project_root = File.expand_path("..", __dir__)
|
||||||
|
aab_path = options[:aab]
|
||||||
|
UI.user_error!("Pass the exact verified artifact with aab: — never release an implicit build") unless aab_path
|
||||||
|
aab_path = File.expand_path(aab_path, project_root) unless aab_path.start_with?("/")
|
||||||
|
UI.user_error!("AAB not found at #{aab_path}") unless File.exist?(aab_path)
|
||||||
|
|
||||||
|
rollout = options[:rollout] # e.g. "0.1" for 10% staged rollout; omit for full rollout
|
||||||
|
|
||||||
|
UI.message("Releasing #{aab_path} to #{track} track#{rollout ? " at #{(rollout.to_f * 100).round}% rollout" : " at full rollout"}...")
|
||||||
|
|
||||||
|
upload_to_play_store(
|
||||||
|
track: track,
|
||||||
|
aab: aab_path,
|
||||||
|
skip_upload_metadata: true,
|
||||||
|
skip_upload_images: true,
|
||||||
|
skip_upload_screenshots: true,
|
||||||
|
skip_upload_changelogs: false,
|
||||||
|
release_status: rollout ? "inProgress" : "completed",
|
||||||
|
rollout: rollout,
|
||||||
|
)
|
||||||
|
|
||||||
|
UI.success("Release committed to #{track}. Google Play review runs automatically; rollout proceeds on approval.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
platform :ios do
|
||||||
|
# App Store Connect API key, supplied via environment (never committed):
|
||||||
|
# ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH (path to the .p8 file)
|
||||||
|
private_lane :asc_api_key do
|
||||||
|
UI.user_error!("Set ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_PATH") unless ENV["ASC_KEY_ID"] && ENV["ASC_ISSUER_ID"] && ENV["ASC_KEY_PATH"]
|
||||||
|
app_store_connect_api_key(
|
||||||
|
key_id: ENV["ASC_KEY_ID"],
|
||||||
|
issuer_id: ENV["ASC_ISSUER_ID"],
|
||||||
|
key_filepath: ENV["ASC_KEY_PATH"],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Upload IPA to App Store Connect / TestFlight only (no App Review submission)"
|
||||||
|
lane :upload do |options|
|
||||||
|
ipa_path = options[:ipa]
|
||||||
|
UI.user_error!("Pass the exact verified artifact with ipa:") unless ipa_path && File.exist?(File.expand_path(ipa_path))
|
||||||
|
|
||||||
|
upload_to_testflight(
|
||||||
|
api_key: asc_api_key,
|
||||||
|
ipa: File.expand_path(ipa_path),
|
||||||
|
skip_waiting_for_build_processing: false,
|
||||||
|
)
|
||||||
|
UI.success("Uploaded to App Store Connect; build is processing for TestFlight.")
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Upload IPA, create the App Store version, and submit it for App Review with automatic release on approval"
|
||||||
|
lane :release do |options|
|
||||||
|
ipa_path = options[:ipa]
|
||||||
|
UI.user_error!("Pass the exact verified artifact with ipa:") unless ipa_path && File.exist?(File.expand_path(ipa_path))
|
||||||
|
|
||||||
|
api_key = asc_api_key
|
||||||
|
|
||||||
|
upload_to_app_store(
|
||||||
|
api_key: api_key,
|
||||||
|
ipa: File.expand_path(ipa_path),
|
||||||
|
submit_for_review: true,
|
||||||
|
automatic_release: options.fetch(:automatic_release, true),
|
||||||
|
phased_release: options.fetch(:phased_release, false),
|
||||||
|
precheck_include_in_app_purchases: false,
|
||||||
|
# Metadata lives in App Store Connect; only release notes change per version.
|
||||||
|
skip_metadata: options[:release_notes].nil?,
|
||||||
|
skip_screenshots: true,
|
||||||
|
release_notes: options[:release_notes] ? { "en-GB" => options[:release_notes], "default" => options[:release_notes] } : nil,
|
||||||
|
submission_information: {
|
||||||
|
add_id_info_uses_idfa: false,
|
||||||
|
export_compliance_uses_encryption: false,
|
||||||
|
},
|
||||||
|
force: true, # skip interactive HTML preview; agents run non-interactively
|
||||||
|
)
|
||||||
|
|
||||||
|
UI.success("Submitted for App Review. The version releases #{options.fetch(:automatic_release, true) ? "automatically on approval" : "manually after approval"}.")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -28,3 +28,6 @@ project.xcworkspace
|
|||||||
|
|
||||||
# CocoaPods
|
# CocoaPods
|
||||||
/Pods/
|
/Pods/
|
||||||
|
|
||||||
|
# @bacons/apple-targets generated entitlements cache
|
||||||
|
/.targets/
|
||||||
@@ -1,15 +1,28 @@
|
|||||||
|
# Set by expo-router. This enables Fabric-only features from react-native-screens
|
||||||
|
ENV['RNS_GAMMA_ENABLED'] ||= '1'
|
||||||
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||||
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||||
|
|
||||||
require 'json'
|
require 'json'
|
||||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||||
|
|
||||||
ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
|
def ccache_enabled?(podfile_properties)
|
||||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
# Environment variable takes precedence
|
||||||
|
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
|
||||||
|
|
||||||
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
# Fall back to Podfile properties
|
||||||
install! 'cocoapods',
|
podfile_properties['apple.ccacheEnabled'] == 'true'
|
||||||
:deterministic_uuids => false
|
end
|
||||||
|
|
||||||
|
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||||
|
ENV['RCT_USE_RN_DEP'] ||= podfile_properties['ios.buildReactNativeFromSource'] == 'true' ? '0' : '1'
|
||||||
|
ENV['RCT_USE_PREBUILT_RNCORE'] ||= podfile_properties['ios.buildReactNativeFromSource'] == 'true' ? '0' : '1'
|
||||||
|
ENV['RCT_HERMES_V1_ENABLED'] ||= '0' if podfile_properties['expo.useHermesV1'] == 'false'
|
||||||
|
# An explicit `ios.usePrecompiledModules: false` must win even when the env var is already set
|
||||||
|
# (e.g. EXPO_USE_PRECOMPILED_MODULES exported by EAS Build), so force-disable here before the default.
|
||||||
|
ENV['EXPO_USE_PRECOMPILED_MODULES'] = '0' if podfile_properties['EXPO_USE_PRECOMPILED_MODULES'] == 'false'
|
||||||
|
ENV['EXPO_USE_PRECOMPILED_MODULES'] ||= '1'
|
||||||
|
platform :ios, podfile_properties['ios.deploymentTarget'] || '16.4'
|
||||||
|
|
||||||
prepare_react_native_project!
|
prepare_react_native_project!
|
||||||
|
|
||||||
@@ -23,7 +36,8 @@ target 'Sauci' do
|
|||||||
'node',
|
'node',
|
||||||
'--no-warnings',
|
'--no-warnings',
|
||||||
'--eval',
|
'--eval',
|
||||||
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
|
'require(\'expo/bin/autolinking\')',
|
||||||
|
'expo-modules-autolinking',
|
||||||
'react-native-config',
|
'react-native-config',
|
||||||
'--json',
|
'--json',
|
||||||
'--platform',
|
'--platform',
|
||||||
@@ -49,34 +63,8 @@ target 'Sauci' do
|
|||||||
installer,
|
installer,
|
||||||
config[:reactNativePath],
|
config[:reactNativePath],
|
||||||
:mac_catalyst_enabled => false,
|
:mac_catalyst_enabled => false,
|
||||||
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
:ccache_enabled => ccache_enabled?(podfile_properties),
|
||||||
)
|
)
|
||||||
|
|
||||||
# The fmt version bundled with React Native 0.76 enables consteval for
|
|
||||||
# Apple Clang, but Apple Clang 21 (Xcode 26.4+) rejects fmt's FMT_STRING
|
|
||||||
# call sites. Patch the generated pod until React Native supplies a newer
|
|
||||||
# fmt release. Keeping this here makes clean pod installs reproducible.
|
|
||||||
fmt_base_header = File.join(__dir__, 'Pods', 'fmt', 'include', 'fmt', 'base.h')
|
|
||||||
if File.exist?(fmt_base_header)
|
|
||||||
fmt_base_source = File.read(fmt_base_header)
|
|
||||||
fmt_base_source.sub!(
|
|
||||||
/#elif defined\(__apple_build_version__\) && __apple_build_version__ < 14000029L/,
|
|
||||||
'#elif defined(__apple_build_version__)'
|
|
||||||
)
|
|
||||||
File.chmod(0644, fmt_base_header)
|
|
||||||
File.write(fmt_base_header, fmt_base_source)
|
|
||||||
end
|
|
||||||
|
|
||||||
# This is necessary for Xcode 14, because it signs resource bundles by default
|
|
||||||
# when building for devices.
|
|
||||||
installer.target_installation_results.pod_target_installation_results
|
|
||||||
.each do |pod_name, target_installation_result|
|
|
||||||
target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
|
|
||||||
resource_bundle_target.build_configurations.each do |config|
|
|
||||||
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"expo.jsEngine": "hermes",
|
"expo.jsEngine": "hermes",
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
"newArchEnabled": "true",
|
"expo.inlineModules.watchedDirectories": "[]",
|
||||||
"ios.useFrameworks": "static",
|
"expo.inlineModules.xcodeProjectTargets": "{\"mainTarget\":\"Sauci\",\"targets\":[]}",
|
||||||
"apple.extraPods": "[]",
|
"ios.forceStaticLinking": "[]",
|
||||||
"apple.ccacheEnabled": "false",
|
"apple.privacyManifestAggregationEnabled": "true",
|
||||||
"apple.privacyManifestAggregationEnabled": "true"
|
"EXPO_USE_PRECOMPILED_MODULES": "true",
|
||||||
|
"ios.useFrameworks": "static"
|
||||||
}
|
}
|
||||||
@@ -3,19 +3,18 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 46;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
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 */; };
|
3F202C2A859975F48A83ADD7 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779267BD380A5D29590F1BDC /* ExpoModulesProvider.swift */; };
|
||||||
7B71EEB2AF682FA7AFF556D5 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDDAD8C12C38D3BD0A76D345 /* Pods_Sauci.framework */; };
|
45941E25D336B6152BFCEFB9 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B482C7D915C22B3F33CA9841 /* Pods_Sauci.framework */; };
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
804CD03F6B584D08B7803C63 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 69E91809C21A4977A3249D53 /* PrivacyInfo.xcprivacy */; };
|
||||||
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 */; };
|
D60EEEF252214D45B67A0063 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D58CE235EF5F40F58FB8111D /* GoogleService-Info.plist */; };
|
||||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
|
||||||
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 */; };
|
||||||
XX3C562DEF346B45594832XX /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XXCAF23C3FC41F82140FA1XX /* ActivityKit.framework */; };
|
XX3C562DEF346B45594832XX /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = XXCAF23C3FC41F82140FA1XX /* ActivityKit.framework */; };
|
||||||
@@ -24,11 +23,11 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
XXA7C7D5580082A497FD95XX /* PBXContainerItemProxy */ = {
|
XX77D941E1DB38490B8F4EXX /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||||
proxyType = 1;
|
proxyType = 1;
|
||||||
remoteGlobalIDString = XX540FB4ABC75CE91A6036XX;
|
remoteGlobalIDString = XXB3405CC208C2ECA4797FXX;
|
||||||
remoteInfo = LiveDrawWidget;
|
remoteInfo = LiveDrawWidget;
|
||||||
};
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
@@ -51,18 +50,17 @@
|
|||||||
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; };
|
||||||
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>"; };
|
||||||
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>"; };
|
69E91809C21A4977A3249D53 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; 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>"; };
|
779267BD380A5D29590F1BDC /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; 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>"; };
|
A172EA788EF476A7083810FE /* Pods-Sauci.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.release.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Sauci/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
B482C7D915C22B3F33CA9841 /* Pods_Sauci.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sauci.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||||
D7B5C17768E3B74AC152531F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = undefined; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
D58CE235EF5F40F58FB8111D /* 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>"; };
|
||||||
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; };
|
|
||||||
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>"; };
|
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>"; };
|
F11748442D0722820044C1D9 /* Sauci-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Sauci-Bridging-Header.h"; path = "Sauci/Sauci-Bridging-Header.h"; 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>"; };
|
FEACE2E5B4EA50AFBF4DDF34 /* Pods-Sauci.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sauci.debug.xcconfig"; path = "Target Support Files/Pods-Sauci/Pods-Sauci.debug.xcconfig"; 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; };
|
||||||
XX4DFF38D47332D6BF0183XX /* SwiftUI.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
XX4DFF38D47332D6BF0183XX /* SwiftUI.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
XXA12889081EB894481655XX /* AppIntents.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = AppIntents.framework; path = System/Library/Frameworks/AppIntents.framework; sourceTree = SDKROOT; };
|
XXA12889081EB894481655XX /* AppIntents.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = AppIntents.framework; path = System/Library/Frameworks/AppIntents.framework; sourceTree = SDKROOT; };
|
||||||
@@ -71,21 +69,21 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
XX020388E3BB9C764E774FXX /* Exceptions for "widget" folder in "LiveDrawWidget" target */ = {
|
XX6FB9E6678087E3AAC658XX /* Exceptions for "widget" folder in "LiveDrawWidget" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
Info.plist,
|
Info.plist,
|
||||||
"expo-target.config.js",
|
"expo-target.config.js",
|
||||||
);
|
);
|
||||||
target = XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */;
|
target = XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */;
|
||||||
};
|
};
|
||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
XX683E29AD7AD8279E17B2XX /* widget */ = {
|
XXBB44BF07EB68666150ABXX /* widget */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
XX020388E3BB9C764E774FXX /* Exceptions for "widget" folder in "LiveDrawWidget" target */,
|
XX6FB9E6678087E3AAC658XX /* Exceptions for "widget" folder in "LiveDrawWidget" target */,
|
||||||
);
|
);
|
||||||
explicitFileTypes = {
|
explicitFileTypes = {
|
||||||
};
|
};
|
||||||
@@ -101,7 +99,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
7B71EEB2AF682FA7AFF556D5 /* Pods_Sauci.framework in Frameworks */,
|
45941E25D336B6152BFCEFB9 /* Pods_Sauci.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -122,15 +120,32 @@
|
|||||||
13B07FAE1A68108700A75B9A /* Sauci */ = {
|
13B07FAE1A68108700A75B9A /* Sauci */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
|
||||||
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
||||||
|
F11748442D0722820044C1D9 /* Sauci-Bridging-Header.h */,
|
||||||
|
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||||
A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */,
|
69E91809C21A4977A3249D53 /* PrivacyInfo.xcprivacy */,
|
||||||
F744EAD4ABA54509966475D6 /* noop-file.swift */,
|
D58CE235EF5F40F58FB8111D /* GoogleService-Info.plist */,
|
||||||
DA536CAC88B349A59DFC1F1E /* Sauci-Bridging-Header.h */,
|
);
|
||||||
D7B5C17768E3B74AC152531F /* PrivacyInfo.xcprivacy */,
|
name = Sauci;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1957149DB83AAF8603C60859 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FEACE2E5B4EA50AFBF4DDF34 /* Pods-Sauci.debug.xcconfig */,
|
||||||
|
A172EA788EF476A7083810FE /* Pods-Sauci.release.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
2BFCD64E7EEB594CC2D9E261 /* Sauci */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
779267BD380A5D29590F1BDC /* ExpoModulesProvider.swift */,
|
||||||
);
|
);
|
||||||
name = Sauci;
|
name = Sauci;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -141,9 +156,9 @@
|
|||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||||
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */,
|
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */,
|
||||||
XX4DFF38D47332D6BF0183XX /* SwiftUI.framework */,
|
XX4DFF38D47332D6BF0183XX /* SwiftUI.framework */,
|
||||||
DDDAD8C12C38D3BD0A76D345 /* Pods_Sauci.framework */,
|
|
||||||
XXCAF23C3FC41F82140FA1XX /* ActivityKit.framework */,
|
XXCAF23C3FC41F82140FA1XX /* ActivityKit.framework */,
|
||||||
XXA12889081EB894481655XX /* AppIntents.framework */,
|
XXA12889081EB894481655XX /* AppIntents.framework */,
|
||||||
|
B482C7D915C22B3F33CA9841 /* Pods_Sauci.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -163,8 +178,8 @@
|
|||||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||||
83CBBA001A601CBA00E9B192 /* Products */,
|
83CBBA001A601CBA00E9B192 /* Products */,
|
||||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||||
D65327D7A22EEC0BE12398D9 /* Pods */,
|
1957149DB83AAF8603C60859 /* Pods */,
|
||||||
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */,
|
CC8FEB5C5178B9E5E71D4F61 /* ExpoModulesProviders */,
|
||||||
);
|
);
|
||||||
indentWidth = 2;
|
indentWidth = 2;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -180,14 +195,6 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
92DBD88DE9BF7D494EA9DA96 /* Sauci */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */,
|
|
||||||
);
|
|
||||||
name = Sauci;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
BB2F792B24A3F905000567C9 /* Supporting */ = {
|
BB2F792B24A3F905000567C9 /* Supporting */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -197,19 +204,10 @@
|
|||||||
path = Sauci/Supporting;
|
path = Sauci/Supporting;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D65327D7A22EEC0BE12398D9 /* Pods */ = {
|
CC8FEB5C5178B9E5E71D4F61 /* ExpoModulesProviders */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */,
|
2BFCD64E7EEB594CC2D9E261 /* Sauci */,
|
||||||
7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */,
|
|
||||||
);
|
|
||||||
path = Pods;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
92DBD88DE9BF7D494EA9DA96 /* Sauci */,
|
|
||||||
);
|
);
|
||||||
name = ExpoModulesProviders;
|
name = ExpoModulesProviders;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -217,7 +215,7 @@
|
|||||||
XX3E41C1850246162C0061XX /* expo:targets */ = {
|
XX3E41C1850246162C0061XX /* expo:targets */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
XX683E29AD7AD8279E17B2XX /* widget */,
|
XXBB44BF07EB68666150ABXX /* widget */,
|
||||||
);
|
);
|
||||||
name = "expo:targets";
|
name = "expo:targets";
|
||||||
path = ../targets;
|
path = ../targets;
|
||||||
@@ -231,30 +229,31 @@
|
|||||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */;
|
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Sauci" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||||
DC3D179C6B62F16DA5F17C2F /* [Expo] Configure project */,
|
E6F7433229E3C1448FC9F062 /* [Expo] Configure project */,
|
||||||
13B07F871A680F5B00A75B9A /* Sources */,
|
13B07F871A680F5B00A75B9A /* Sources */,
|
||||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||||
|
D58CF933D4804A0CB49C2891 /* [Expo Dev Launcher] Strip Local Network Keys for Release */,
|
||||||
XX72A8CFBF0FBA13A79902XX /* Embed Foundation Extensions */,
|
XX72A8CFBF0FBA13A79902XX /* Embed Foundation Extensions */,
|
||||||
A1248897D24903117793599B /* [CP] Embed Pods Frameworks */,
|
503A0FD3DBA862B4BE649BBB /* [CP] Embed Pods Frameworks */,
|
||||||
FB4B019D835055733ACDE109 /* [CP-User] [RNFB] Core Configuration */,
|
8490E10253F91A7BEB80F555 /* [CP-User] [RNFB] Core Configuration */,
|
||||||
563ACE2651AC39438F3B399B /* [CP-User] [RNFB] Crashlytics Configuration */,
|
0EE1B67C9AAD8BD8562E800E /* [CP-User] [RNFB] Crashlytics Configuration */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
XX97289CEC7994AD5F4142XX /* PBXTargetDependency */,
|
XX5D3A21868E45E6E5BDC3XX /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Sauci;
|
name = Sauci;
|
||||||
productName = Sauci;
|
productName = Sauci;
|
||||||
productReference = 13B07F961A680F5B00A75B9A /* Sauci.app */;
|
productReference = 13B07F961A680F5B00A75B9A /* Sauci.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */ = {
|
XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = XX63FAB1BD532DA0D8AE18XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */;
|
buildConfigurationList = XX35D62F4FBF2C9086E8C9XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
XX074B8811C1CF8E8A3D74XX /* Frameworks */,
|
XX074B8811C1CF8E8A3D74XX /* Frameworks */,
|
||||||
XXE45443DC38DCFE1DB622XX /* Sources */,
|
XXE45443DC38DCFE1DB622XX /* Sources */,
|
||||||
@@ -265,7 +264,7 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
XX683E29AD7AD8279E17B2XX /* widget */,
|
XXBB44BF07EB68666150ABXX /* widget */,
|
||||||
);
|
);
|
||||||
name = LiveDrawWidget;
|
name = LiveDrawWidget;
|
||||||
productName = LiveDrawWidget;
|
productName = LiveDrawWidget;
|
||||||
@@ -283,7 +282,7 @@
|
|||||||
13B07F861A680F5B00A75B9A = {
|
13B07F861A680F5B00A75B9A = {
|
||||||
LastSwiftMigration = 1250;
|
LastSwiftMigration = 1250;
|
||||||
};
|
};
|
||||||
XX540FB4ABC75CE91A6036XX = {
|
XXB3405CC208C2ECA4797FXX = {
|
||||||
CreatedOnToolsVersion = 14.3;
|
CreatedOnToolsVersion = 14.3;
|
||||||
DevelopmentTeam = BXVPBATDZY;
|
DevelopmentTeam = BXVPBATDZY;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
@@ -304,7 +303,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
13B07F861A680F5B00A75B9A /* Sauci */,
|
13B07F861A680F5B00A75B9A /* Sauci */,
|
||||||
XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */,
|
XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -317,8 +316,8 @@
|
|||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||||
FDFABE1A1A3547B1B22DD2DD /* GoogleService-Info.plist in Resources */,
|
804CD03F6B584D08B7803C63 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
CEF2F278ABBC5FBFD470A0F7 /* PrivacyInfo.xcprivacy in Resources */,
|
D60EEEF252214D45B67A0063 /* GoogleService-Info.plist in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -341,6 +340,8 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"$(SRCROOT)/.xcode.env",
|
||||||
|
"$(SRCROOT)/.xcode.env.local",
|
||||||
);
|
);
|
||||||
name = "Bundle React Native code and images";
|
name = "Bundle React Native code and images";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
@@ -373,7 +374,7 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
563ACE2651AC39438F3B399B /* [CP-User] [RNFB] Crashlytics Configuration */ = {
|
0EE1B67C9AAD8BD8562E800E /* [CP-User] [RNFB] Crashlytics Configuration */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -387,6 +388,52 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n";
|
shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n";
|
||||||
};
|
};
|
||||||
|
503A0FD3DBA862B4BE649BBB /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem.framework/ExpoFileSystem",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoFont/ExpoFont.framework/ExpoFont",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoImage/ExpoImage.framework/ExpoImage",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoImage/SDWebImage.framework/SDWebImage",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoImage/SDWebImageAVIFCoder.framework/SDWebImageAVIFCoder",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoImage/SDWebImageSVGCoder.framework/SDWebImageSVGCoder",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoImage/SDWebImageWebPCoder.framework/SDWebImageWebPCoder",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoImage/libavif.framework/libavif",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary.framework/ExpoMediaLibrary",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoModulesCore/ExpoModulesCore.framework/ExpoModulesCore",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoModulesJSI/ExpoModulesJSI.framework/ExpoModulesJSI",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ExpoModulesWorklets/ExpoModulesWorklets.framework/ExpoModulesWorklets",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
|
||||||
|
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoFileSystem.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoFont.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoImage.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageAVIFCoder.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageSVGCoder.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavif.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoMediaLibrary.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoModulesCore.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoModulesJSI.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ExpoModulesWorklets.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -398,9 +445,10 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoNotifications/ExpoNotifications_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreExtension/FirebaseCoreExtension_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCoreInternal/FirebaseCoreInternal_Privacy.bundle",
|
||||||
@@ -414,18 +462,16 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle",
|
||||||
"${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}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.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",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-view-shot/RNViewShotPrivacyInfo.bundle",
|
||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
@@ -433,9 +479,10 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoMediaLibrary_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoMediaLibrary_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCore_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreExtension_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FirebaseCoreInternal_Privacy.bundle",
|
||||||
@@ -449,43 +496,54 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle",
|
||||||
"${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}/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}/RNSVGFilters.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.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",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNViewShotPrivacyInfo.bundle",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
A1248897D24903117793599B /* [CP] Embed Pods Frameworks */ = {
|
8490E10253F91A7BEB80F555 /* [CP-User] [RNFB] Core Configuration */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh",
|
"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
|
||||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
);
|
||||||
|
name = "[CP-User] [RNFB] Core Configuration";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACLine truncated
|
||||||
|
};
|
||||||
|
D58CF933D4804A0CB49C2891 /* [Expo Dev Launcher] Strip Local Network Keys for Release */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "[Expo Dev Launcher] Strip Local Network Keys for Release";
|
||||||
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh\"\n";
|
shellScript = "# Strip dev-launcher-specific local network permission keys from non-Debug builds\n# This only removes _expo._tcp Bonjour services and the dev-launcher usage description.\n# Other Bonjour services and custom descriptions are preserved for production use.\n\nif [ \"$CONFIGURATION\" != \"Debug\" ]; then\n PLIST_PATH=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n if [ -f \"$PLIST_PATH\" ]; then\n # Check if NSBonjourServices exists\n if /usr/libexec/PlistBuddy -c \"Print :NSBonjourServices\" \"$PLIST_PATH\" >/dev/null 2>&1; then\n # Get the count of services\n COUNT=$(/usr/libexec/PlistBuddy -c \"Print :NSBonjourServices\" \"$PLIST_PATH\" 2>/dev/null | grep \"^ \" | wc -l | tr -d ' ')\n\n # Remove _expo._tcp\n for ((i=COUNT-1; i>=0; i--)); do\n SERVICE=$(/usr/libexec/PlistBuddy -c \"Print :NSBonjourServices:$i\" \"$PLIST_PATH\" 2>/dev/null || echo \"\")\n if echo \"$SERVICE\" | grep -q \"_expo._tcp\"; then\n /usr/libexec/PlistBuddy -c \"Delete :NSBonjourServices:$i\" \"$PLIST_PATH\" 2>/dev/null || true\n fi\n done\n\n # If the array is now empty, remove it entirely\n REMAINING=$(/usr/libexec/PlistBuddy -c \"Print :NSBonjourServices\" \"$PLIST_PATH\" 2>/dev/null | grep \"^ \" | wc -l | tr -d ' ')\n if [ \"$REMAINING\" -eq \"0\" ]; then\n /usr/libexec/PlistBuddy -c \"Delete :NSBonjourServices\" \"$PLIST_PATH\" 2>/dev/null || true\n fi\n fi\n\n # Only delete the description if it matches the dev-launcher default text\n DESC=$(/usr/libexec/PlistBuddy -c \"Print :NSLocalNetworkUsageDescription\" \"$PLIST_PATH\" 2>/dev/null || echo \"\")\n if echo \"$DESC\" | grep -q \"Expo Dev Launcher\"; then\n /usr/libexec/PlistBuddy -c \"Delete :NSLocalNetworkUsageDescription\" \"$PLIST_PATH\" 2>/dev/null || true\n fi\n fi\nfi\n";
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
};
|
||||||
DC3D179C6B62F16DA5F17C2F /* [Expo] Configure project */ = {
|
E6F7433229E3C1448FC9F062 /* [Expo] Configure project */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -509,19 +567,6 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Sauci/expo-configure-project.sh\"\n";
|
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Sauci/expo-configure-project.sh\"\n";
|
||||||
};
|
};
|
||||||
FB4B019D835055733ACDE109 /* [CP-User] [RNFB] Core Configuration */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
|
|
||||||
);
|
|
||||||
name = "[CP-User] [RNFB] Core Configuration";
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACLine truncated
|
|
||||||
};
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -530,8 +575,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
||||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
3F202C2A859975F48A83ADD7 /* ExpoModulesProvider.swift in Sources */,
|
||||||
5254BDADE70741A0B2504FB1 /* noop-file.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -545,17 +589,17 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
XX97289CEC7994AD5F4142XX /* PBXTargetDependency */ = {
|
XX5D3A21868E45E6E5BDC3XX /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */;
|
target = XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */;
|
||||||
targetProxy = XXA7C7D5580082A497FD95XX /* PBXContainerItemProxy */;
|
targetProxy = XX77D941E1DB38490B8F4EXX /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */;
|
baseConfigurationReference = FEACE2E5B4EA50AFBF4DDF34 /* Pods-Sauci.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
@@ -568,9 +612,13 @@
|
|||||||
"FB_SONARKIT_ENABLED=1",
|
"FB_SONARKIT_ENABLED=1",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
MARKETING_VERSION = 1.0.7;
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.4;
|
||||||
|
MARKETING_VERSION = 1.0.8;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -589,7 +637,7 @@
|
|||||||
};
|
};
|
||||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */;
|
baseConfigurationReference = A172EA788EF476A7083810FE /* Pods-Sauci.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
@@ -597,9 +645,13 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
MARKETING_VERSION = 1.0.7;
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 13.4;
|
||||||
|
MARKETING_VERSION = 1.0.8;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -664,29 +716,39 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
"${PODS_ROOT}/ReactCommon",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
"${PODS_ROOT}/ReactCommon/react/nativemodule/core",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
"${PODS_ROOT}/React-runtimeexecutor",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
"${PODS_ROOT}/React-runtimeexecutor/platform/ios",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
"${PODS_ROOT}/ReactCommon-Samples",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
"${PODS_ROOT}/ReactCommon-Samples/platform/ios",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
"${PODS_ROOT}/React-Fabric/react/renderer/components/view/platform/cxx",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
"${PODS_ROOT}/React-NativeModulesApple",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers",
|
"${PODS_ROOT}/React-graphics",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios",
|
"${PODS_ROOT}/React-graphics/react/renderer/graphics/platform/ios",
|
||||||
|
"${PODS_ROOT}/React-featureflags",
|
||||||
|
"${PODS_ROOT}/React-renderercss",
|
||||||
|
);
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
/usr/lib/swift,
|
||||||
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_CFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
" ",
|
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||||
|
);
|
||||||
|
OTHER_CPLUSPLUSFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||||
);
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -733,33 +795,43 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
"${PODS_ROOT}/ReactCommon",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
"${PODS_ROOT}/ReactCommon/react/nativemodule/core",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
"${PODS_ROOT}/React-runtimeexecutor",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
"${PODS_ROOT}/React-runtimeexecutor/platform/ios",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
"${PODS_ROOT}/ReactCommon-Samples",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
"${PODS_ROOT}/ReactCommon-Samples/platform/ios",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
"${PODS_ROOT}/React-Fabric/react/renderer/components/view/platform/cxx",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
"${PODS_ROOT}/React-NativeModulesApple",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers",
|
"${PODS_ROOT}/React-graphics",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios",
|
"${PODS_ROOT}/React-graphics/react/renderer/graphics/platform/ios",
|
||||||
|
"${PODS_ROOT}/React-featureflags",
|
||||||
|
"${PODS_ROOT}/React-renderercss",
|
||||||
|
);
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
/usr/lib/swift,
|
||||||
|
"$(inherited)",
|
||||||
);
|
);
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_CFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
" ",
|
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||||
|
);
|
||||||
|
OTHER_CPLUSPLUSFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||||
);
|
);
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
XX1230635DC14DD95E28A2XX /* Release */ = {
|
XX0A0FB078256184BF7EA2XX /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
@@ -772,7 +844,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 = 46;
|
CURRENT_PROJECT_VERSION = 49;
|
||||||
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;
|
||||||
@@ -781,12 +853,17 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = LiveDrawWidget;
|
INFOPLIST_KEY_CFBundleDisplayName = LiveDrawWidget;
|
||||||
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 = (
|
||||||
MARKETING_VERSION = 1.0.7;
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0.8;
|
||||||
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;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
REGISTER_APP_GROUPS = YES;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -796,7 +873,7 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
XX710DB962925BCE8D59ACXX /* Debug */ = {
|
XX42CD38636C47E26CBE73XX /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
@@ -808,7 +885,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 = 46;
|
CURRENT_PROJECT_VERSION = 49;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@@ -817,13 +894,18 @@
|
|||||||
INFOPLIST_KEY_CFBundleDisplayName = LiveDrawWidget;
|
INFOPLIST_KEY_CFBundleDisplayName = LiveDrawWidget;
|
||||||
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 = (
|
||||||
MARKETING_VERSION = 1.0.7;
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0.8;
|
||||||
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";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app.LiveDrawWidget;
|
PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app.LiveDrawWidget;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
REGISTER_APP_GROUPS = YES;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -854,11 +936,11 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
XX63FAB1BD532DA0D8AE18XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */ = {
|
XX35D62F4FBF2C9086E8C9XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
XX710DB962925BCE8D59ACXX /* Debug */,
|
XX42CD38636C47E26CBE73XX /* Debug */,
|
||||||
XX1230635DC14DD95E28A2XX /* Release */,
|
XX0A0FB078256184BF7EA2XX /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import Expo
|
internal import Expo
|
||||||
import FirebaseCore
|
import FirebaseCore
|
||||||
import React
|
import React
|
||||||
import ReactAppDependencyProvider
|
import ReactAppDependencyProvider
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
public class AppDelegate: ExpoAppDelegate {
|
class AppDelegate: ExpoAppDelegate {
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
|
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
|
||||||
@@ -20,11 +20,12 @@ public class AppDelegate: ExpoAppDelegate {
|
|||||||
|
|
||||||
reactNativeDelegate = delegate
|
reactNativeDelegate = delegate
|
||||||
reactNativeFactory = factory
|
reactNativeFactory = factory
|
||||||
bindReactNativeFactory(factory)
|
|
||||||
|
|
||||||
#if os(iOS) || os(tvOS)
|
#if os(iOS) || os(tvOS)
|
||||||
window = UIWindow(frame: UIScreen.main.bounds)
|
window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
FirebaseApp.configure()
|
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a
|
||||||
|
FirebaseApp.configure()
|
||||||
|
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
|
||||||
factory.startReactNative(
|
factory.startReactNative(
|
||||||
withModuleName: "main",
|
withModuleName: "main",
|
||||||
in: window,
|
in: window,
|
||||||
@@ -34,42 +35,39 @@ public class AppDelegate: ExpoAppDelegate {
|
|||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Linking API
|
||||||
public override func application(
|
public override func application(
|
||||||
_ app: UIApplication,
|
_ app: UIApplication,
|
||||||
open url: URL,
|
open url: URL,
|
||||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
super.application(app, open: url, options: options)
|
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
|
||||||
|| RCTLinkingManager.application(app, open: url, options: options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Universal Links
|
||||||
public override func application(
|
public override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
continue userActivity: NSUserActivity,
|
continue userActivity: NSUserActivity,
|
||||||
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
let result = RCTLinkingManager.application(
|
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||||
application,
|
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
|
||||||
continue: userActivity,
|
|
||||||
restorationHandler: restorationHandler)
|
|
||||||
return super.application(
|
|
||||||
application,
|
|
||||||
continue: userActivity,
|
|
||||||
restorationHandler: restorationHandler) || result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
|
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
|
||||||
|
// Extension point for config-plugins
|
||||||
|
|
||||||
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||||||
|
// needed to return the correct URL for expo-dev-client.
|
||||||
bridge.bundleURL ?? bundleURL()
|
bridge.bundleURL ?? bundleURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func bundleURL() -> URL? {
|
override func bundleURL() -> URL? {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
RCTBundleURLProvider.sharedSettings()
|
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
||||||
.jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
|
||||||
#else
|
#else
|
||||||
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 57 KiB |
@@ -21,7 +21,7 @@
|
|||||||
<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.7</string>
|
<string>1.0.8</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>46</string>
|
<string>49</string>
|
||||||
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
@@ -103,13 +103,6 @@
|
|||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIUserInterfaceStyle</key>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<string>Automatic</string>
|
<string>Automatic</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
|||||||
@@ -24,6 +24,14 @@
|
|||||||
<string>C56D.1</string>
|
<string>C56D.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>35F9.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
@@ -33,18 +41,171 @@
|
|||||||
<string>85F4.1</string>
|
<string>85F4.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>35F9.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
</array>
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
<array/>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeEmailAddress</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeName</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeSensitiveInfo</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeProductPersonalization</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeUserID</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeDeviceID</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePhotosorVideos</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeAudioData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePerformanceData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurchaseHistory</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>NSPrivacyTracking</key>
|
<key>NSPrivacyTracking</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSPrivacyTrackingDomains</key>
|
||||||
|
<array/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
//
|
//
|
||||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||||
//
|
//
|
||||||
@@ -17,4 +17,4 @@
|
|||||||
<string>group.com.sauci.app</string>
|
<string>group.com.sauci.app</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
|
||||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24053.1"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
@@ -17,15 +18,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLogo" image="SplashScreenLogo" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
|
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLogo" image="SplashScreenLogo" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
|
||||||
<rect key="frame" x="0" y="0" width="414" height="736"/>
|
<rect key="frame" x="146.5" y="376" width="100" height="100"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
|
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="centerX" secondItem="EXPO-ContainerView" secondAttribute="centerX" id="cad2ab56f97c5429bf29decf850647a4216861d4"/>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="centerY" secondItem="EXPO-ContainerView" secondAttribute="centerY" id="1a145271b085b6ce89b1405a310f5b1bb7656595"/>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
|
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
|
|
||||||
</constraints>
|
</constraints>
|
||||||
<color key="backgroundColor" name="SplashScreenBackground"/>
|
<color key="backgroundColor" name="SplashScreenBackground"/>
|
||||||
</view>
|
</view>
|
||||||
@@ -36,7 +35,10 @@
|
|||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="SplashScreenLogo" width="414" height="736"/>
|
<image name="SplashScreenLogo" width="100" height="100"/>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
<namedColor name="SplashScreenBackground">
|
<namedColor name="SplashScreenBackground">
|
||||||
<color alpha="1.000" blue="0.180392156862745" green="0.101960784313725" red="0.101960784313725" customColorSpace="sRGB" colorSpace="custom"/>
|
<color alpha="1.000" blue="0.180392156862745" green="0.101960784313725" red="0.101960784313725" customColorSpace="sRGB" colorSpace="custom"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ Ug==
|
|||||||
<key>alg</key>
|
<key>alg</key>
|
||||||
<string>rsa-v1_5-sha256</string>
|
<string>rsa-v1_5-sha256</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>EXUpdatesEnableBsdiffPatchSupport</key>
|
||||||
|
<true/>
|
||||||
<key>EXUpdatesEnabled</key>
|
<key>EXUpdatesEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>EXUpdatesLaunchWaitMs</key>
|
<key>EXUpdatesLaunchWaitMs</key>
|
||||||
@@ -45,7 +47,7 @@ Ug==
|
|||||||
<string/>
|
<string/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>EXUpdatesRuntimeVersion</key>
|
<key>EXUpdatesRuntimeVersion</key>
|
||||||
<string>1.0.7</string>
|
<string>1.0.8</string>
|
||||||
<key>EXUpdatesURL</key>
|
<key>EXUpdatesURL</key>
|
||||||
<string>https://ota.apps.jbrunton.co.uk/manifest</string>
|
<string>https://ota.apps.jbrunton.co.uk/manifest</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
//
|
|
||||||
// @generated
|
|
||||||
// A blank Swift file must be created for native modules with Swift files to work correctly.
|
|
||||||
//
|
|
||||||
@@ -12,6 +12,12 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@/(.*)$': '<rootDir>/src/$1',
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
|
// react-native-worklets ships a JS-only mock for its native module at
|
||||||
|
// this path (react-native-reanimated depends on it internally and has
|
||||||
|
// no equivalent of its own). Without this, requiring the real
|
||||||
|
// react-native-worklets under Jest throws because no native module is
|
||||||
|
// registered.
|
||||||
|
'^react-native-worklets$': '<rootDir>/../../node_modules/react-native-worklets/src/mock.ts',
|
||||||
},
|
},
|
||||||
testMatch: [
|
testMatch: [
|
||||||
'<rootDir>/src/**/__tests__/**/*.(spec|test).(ts|tsx)',
|
'<rootDir>/src/**/__tests__/**/*.(spec|test).(ts|tsx)',
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
"release:android": "dotenv -e .env -- eas build --platform android --profile production --local",
|
"release:android": "dotenv -e .env -- eas build --platform android --profile production --local",
|
||||||
"release:android:preview": "dotenv -e .env -- eas build --platform android --profile preview --local",
|
"release:android:preview": "dotenv -e .env -- eas build --platform android --profile preview --local",
|
||||||
"release:android:upload": "bundle exec fastlane android upload",
|
"release:android:upload": "bundle exec fastlane android upload",
|
||||||
|
"release:android:ship": "bundle exec fastlane android release",
|
||||||
|
"release:ios:upload": "bundle exec fastlane ios upload",
|
||||||
|
"release:ios:ship": "bundle exec fastlane ios release",
|
||||||
|
"release:preflight": "node scripts/release-preflight.mjs",
|
||||||
|
"release:preflight:local-eas": "node scripts/release-preflight.mjs --local-eas",
|
||||||
"ota:preflight:staging": "node scripts/ota-publish.mjs staging --preflight",
|
"ota:preflight:staging": "node scripts/ota-publish.mjs staging --preflight",
|
||||||
"ota:preflight:production": "node scripts/ota-publish.mjs production --preflight",
|
"ota:preflight:production": "node scripts/ota-publish.mjs production --preflight",
|
||||||
"ota:publish:staging": "node scripts/ota-publish.mjs staging",
|
"ota:publish:staging": "node scripts/ota-publish.mjs staging",
|
||||||
@@ -29,7 +34,7 @@
|
|||||||
"ota:doctor:production": "node scripts/ota-publish.mjs production --doctor"
|
"ota:doctor:production": "node scripts/ota-publish.mjs production --doctor"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bacons/apple-targets": "^0.1.6",
|
"@bacons/apple-targets": "^5.0.0",
|
||||||
"@bittingz/expo-widgets": "^3.0.2",
|
"@bittingz/expo-widgets": "^3.0.2",
|
||||||
"@expo/ngrok": "^4.1.0",
|
"@expo/ngrok": "^4.1.0",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
@@ -39,54 +44,55 @@
|
|||||||
"@react-native-firebase/app": "^23.8.2",
|
"@react-native-firebase/app": "^23.8.2",
|
||||||
"@react-native-firebase/crashlytics": "^23.8.4",
|
"@react-native-firebase/crashlytics": "^23.8.4",
|
||||||
"@react-native-google-signin/google-signin": "^16.1.1",
|
"@react-native-google-signin/google-signin": "^16.1.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
"@shopify/react-native-skia": "^2.6.9",
|
||||||
"@react-navigation/native": "^7.0.0",
|
|
||||||
"@react-navigation/native-stack": "^7.0.0",
|
|
||||||
"@shopify/react-native-skia": "2.2.12",
|
|
||||||
"@supabase/supabase-js": "^2.39.0",
|
"@supabase/supabase-js": "^2.39.0",
|
||||||
"@tanstack/react-query": "^5.17.0",
|
"@tanstack/react-query": "^5.17.0",
|
||||||
"babel-preset-expo": "~54.0.12",
|
"babel-preset-expo": "~56.0.0",
|
||||||
"base64-arraybuffer": "^1.0.2",
|
"base64-arraybuffer": "^1.0.2",
|
||||||
"expo": "~54.0.0",
|
"expo": "^56.0.0",
|
||||||
"expo-apple-authentication": "~8.0.8",
|
"expo-apple-authentication": "~56.0.4",
|
||||||
"expo-asset": "~12.0.13",
|
"expo-asset": "~56.0.24",
|
||||||
"expo-av": "~16.0.8",
|
"expo-audio": "~56.0.13",
|
||||||
"expo-blur": "~15.0.8",
|
"expo-blur": "~56.0.4",
|
||||||
"expo-build-properties": "~1.0.10",
|
"expo-build-properties": "~56.0.27",
|
||||||
"expo-clipboard": "~8.0.8",
|
"expo-clipboard": "~56.0.4",
|
||||||
"expo-constants": "~18.0.14",
|
"expo-constants": "~56.0.25",
|
||||||
"expo-dev-client": "~6.0.21",
|
"expo-dev-client": "~56.0.26",
|
||||||
"expo-file-system": "~19.0.24",
|
"expo-file-system": "~56.0.11",
|
||||||
"expo-font": "~14.0.12",
|
"expo-font": "~56.0.7",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~56.0.3",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~56.0.12",
|
||||||
"expo-image-picker": "~17.0.11",
|
"expo-image-picker": "~56.0.25",
|
||||||
"expo-linear-gradient": "~15.0.8",
|
"expo-linear-gradient": "~56.0.4",
|
||||||
"expo-linking": "~8.0.12",
|
"expo-linking": "~56.0.17",
|
||||||
"expo-local-authentication": "~17.0.9",
|
"expo-local-authentication": "~56.0.5",
|
||||||
"expo-media-library": "~18.2.1",
|
"expo-media-library": "~56.0.11",
|
||||||
"expo-notifications": "~0.32.17",
|
"expo-notifications": "~56.0.25",
|
||||||
"expo-router": "~6.0.24",
|
"expo-observe": "~56.0.29",
|
||||||
"expo-secure-store": "~15.0.8",
|
"expo-router": "~56.2.20",
|
||||||
"expo-sharing": "~14.0.8",
|
"expo-secure-store": "~56.0.4",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-sharing": "~56.0.26",
|
||||||
"expo-updates": "~29.0.20",
|
"expo-splash-screen": "~56.0.15",
|
||||||
"react": "19.1.0",
|
"expo-status-bar": "~56.0.4",
|
||||||
"react-dom": "19.1.0",
|
"expo-system-ui": "~56.0.5",
|
||||||
"react-native": "0.81.5",
|
"expo-updates": "~56.0.26",
|
||||||
|
"expo-video": "~56.1.4",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"react-native": "0.85.3",
|
||||||
"react-native-android-widget": "^0.22.1",
|
"react-native-android-widget": "^0.22.1",
|
||||||
"react-native-compressor": "^1.13.0",
|
"react-native-compressor": "^1.13.0",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.31.1",
|
||||||
"react-native-purchases": "^9.6.12",
|
"react-native-purchases": "^9.6.12",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "4.3.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.26.0",
|
||||||
"react-native-share": "^12.3.1",
|
"react-native-share": "^12.3.1",
|
||||||
"react-native-svg": "15.12.1",
|
"react-native-svg": "15.15.4",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-view-shot": "^4.0.3",
|
"react-native-view-shot": "5.1.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.5.1",
|
"react-native-worklets": "0.8.3",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -94,15 +100,16 @@
|
|||||||
"@testing-library/jest-native": "^5.4.3",
|
"@testing-library/jest-native": "^5.4.3",
|
||||||
"@testing-library/react-native": "^13.3.3",
|
"@testing-library/react-native": "^13.3.3",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/react": "~19.1.10",
|
"@types/node": "^20.19.0",
|
||||||
|
"@types/react": "~19.2.14",
|
||||||
"@types/react-test-renderer": "^19.1.0",
|
"@types/react-test-renderer": "^19.1.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"eoas": "^3.1.3",
|
"eoas": "^3.1.3",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
"eslint-config-expo": "~56.0.4",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expo": "~54.0.18",
|
"jest-expo": "~56.0.5",
|
||||||
"react-test-renderer": "19.1.0",
|
"react-test-renderer": "19.2.3",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
const projectDir = resolve(import.meta.dirname, '..');
|
const projectDir = resolve(import.meta.dirname, '..');
|
||||||
@@ -140,4 +141,8 @@ export function main(args = process.argv.slice(2), environment = process.env) {
|
|||||||
run('npx', ['eoas', 'publish', '--branch', branch, ...parsePublishArgs(rawPublishArgs)], exportEnv);
|
run('npx', ['eoas', 'publish', '--branch', branch, ...parsePublishArgs(rawPublishArgs)], exportEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv[1] === new URL(import.meta.url).pathname) main();
|
// Compare real filesystem paths. A URL pathname percent-encodes spaces, so
|
||||||
|
// comparing it to process.argv[1] silently skips main() under any checkout
|
||||||
|
// whose absolute path contains a space, and every ota:* script then exits 0
|
||||||
|
// having done nothing.
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { existsSync, readFileSync, statSync, statfsSync } from 'node:fs';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { resolve, join } from 'node:path';
|
||||||
|
|
||||||
|
const projectDir = resolve(import.meta.dirname, '..');
|
||||||
|
const repoRoot = resolve(projectDir, '../..');
|
||||||
|
const certificatePath = 'apps/mobile/certs/certificate.pem';
|
||||||
|
const semverPattern = /^\d+\.\d+\.\d+$/;
|
||||||
|
const minimumFreeBuildBytes = 20 * 1024 ** 3;
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
throw new Error(`Mobile release preflight: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function plistValue(source, key, label) {
|
||||||
|
const values = [...source.matchAll(new RegExp(`<key>${key}</key>\\s*<string>([^<]+)</string>`, 'g'))].map((match) => match[1]);
|
||||||
|
if (values.length !== 1) fail(`expected one ${label} ${key}`);
|
||||||
|
return values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function allMatches(source, expression, label) {
|
||||||
|
const values = [...source.matchAll(expression)].map((match) => match[1].trim());
|
||||||
|
if (!values.length) fail(`found no ${label}`);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitStatus(args, root = repoRoot) {
|
||||||
|
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
||||||
|
if (result.error) fail(`could not run git ${args.join(' ')}: ${result.error.message}`);
|
||||||
|
return result.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateReleaseSources({ root = repoRoot, isTracked } = {}) {
|
||||||
|
const file = (relative) => readFileSync(join(root, relative), 'utf8');
|
||||||
|
const app = JSON.parse(file('apps/mobile/app.json')).expo;
|
||||||
|
const appConfig = file('apps/mobile/app.config.js');
|
||||||
|
const androidGradle = file('apps/mobile/android/app/build.gradle');
|
||||||
|
const androidStrings = file('apps/mobile/android/app/src/main/res/values/strings.xml');
|
||||||
|
const xcode = file('apps/mobile/ios/Sauci.xcodeproj/project.pbxproj');
|
||||||
|
const androidManifest = file('apps/mobile/android/app/src/main/AndroidManifest.xml');
|
||||||
|
const iosInfo = file('apps/mobile/ios/Sauci/Info.plist');
|
||||||
|
const widgetInfo = file('apps/mobile/targets/widget/Info.plist');
|
||||||
|
const iosExpo = file('apps/mobile/ios/Sauci/Supporting/Expo.plist');
|
||||||
|
const publicVersion = app.version;
|
||||||
|
const appConfigRuntime = allMatches(appConfig, /runtimeVersion:\s*['"]([^'"]+)['"]/g, 'explicit app.config.js runtimeVersion');
|
||||||
|
const androidVersion = allMatches(androidGradle, /\bversionName\s+["']([^"']+)["']/g, 'Android versionName');
|
||||||
|
const androidRuntime = allMatches(androidStrings, /<string name="expo_runtime_version">([^<]+)<\/string>/g, 'Android Expo runtime version');
|
||||||
|
const xcodeMarketing = allMatches(xcode, /\bMARKETING_VERSION\s*=\s*([^;]+);/g, 'iOS MARKETING_VERSION settings');
|
||||||
|
const widgetBuildSettings = [...xcode.matchAll(/CURRENT_PROJECT_VERSION\s*=\s*([^;]+);[\s\S]{0,900}?PRODUCT_BUNDLE_IDENTIFIER\s*=\s*com\.sauci\.app\.LiveDrawWidget;/g)].map((match) => match[1].trim());
|
||||||
|
if (widgetBuildSettings.length !== 2) fail('expected Debug and Release LiveDrawWidget CURRENT_PROJECT_VERSION settings');
|
||||||
|
|
||||||
|
const versions = [
|
||||||
|
publicVersion,
|
||||||
|
...appConfigRuntime,
|
||||||
|
...androidVersion,
|
||||||
|
...androidRuntime,
|
||||||
|
...xcodeMarketing,
|
||||||
|
plistValue(iosInfo, 'CFBundleShortVersionString', 'iOS app Info.plist'),
|
||||||
|
plistValue(widgetInfo, 'CFBundleShortVersionString', 'widget Info.plist'),
|
||||||
|
plistValue(iosExpo, 'EXUpdatesRuntimeVersion', 'iOS Expo.plist'),
|
||||||
|
];
|
||||||
|
if (!versions.every((version) => semverPattern.test(version))) fail(`versions must be numeric SemVer: ${versions.join(', ')}`);
|
||||||
|
if (new Set(versions).size !== 1) fail(`public or OTA runtime versions disagree: ${versions.join(', ')}`);
|
||||||
|
|
||||||
|
const appBuild = plistValue(iosInfo, 'CFBundleVersion', 'iOS app Info.plist');
|
||||||
|
const widgetBuild = plistValue(widgetInfo, 'CFBundleVersion', 'widget Info.plist');
|
||||||
|
if (!/^\d+$/.test(appBuild) || !/^\d+$/.test(widgetBuild)) fail('iOS build numbers must be positive integers');
|
||||||
|
if (appBuild !== widgetBuild || widgetBuildSettings.some((build) => build !== appBuild)) {
|
||||||
|
fail(`iOS app, widget, and effective widget Xcode build numbers disagree: app ${appBuild}; widget plist ${widgetBuild}; widget settings ${widgetBuildSettings.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const permission of ['READ_MEDIA_IMAGES', 'READ_MEDIA_VIDEO']) {
|
||||||
|
const removed = new RegExp(`<uses-permission[^>]*android:name="android\\.permission\\.${permission}"[^>]*tools:node="remove"`).test(androidManifest);
|
||||||
|
if (!removed) fail(`AndroidManifest.xml must keep tools:node="remove" on ${permission} (expo prebuild regresses this)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forbiddenPlistKeys = ['NSBonjourServices', 'NSLocalNetworkUsageDescription', 'RCTMetroPort'];
|
||||||
|
const foundDevLauncherKeys = forbiddenPlistKeys.filter((key) => iosInfo.includes(key));
|
||||||
|
if (foundDevLauncherKeys.length) {
|
||||||
|
fail(`ios/Sauci/Info.plist must not contain dev-launcher-only keys reintroduced by expo prebuild: ${foundDevLauncherKeys.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracked = isTracked ?? ((relative) => gitStatus(['ls-files', '--error-unmatch', relative], root) === 0);
|
||||||
|
if (!tracked(certificatePath)) fail(`${certificatePath} must be tracked`);
|
||||||
|
const archiveIgnore = existsSync(join(root, '.easignore')) ? '.easignore' : '.gitignore';
|
||||||
|
if (!existsSync(join(root, archiveIgnore))) fail(`missing root ${archiveIgnore} archive rules`);
|
||||||
|
const archiveRules = file(archiveIgnore).split(/\r?\n/).map((line) => line.trim());
|
||||||
|
const pemRules = archiveRules.map((line, index) => line === '*.pem' ? index : -1).filter((index) => index >= 0);
|
||||||
|
const certificateAllowlist = archiveRules.lastIndexOf(`!${certificatePath}`);
|
||||||
|
if (!pemRules.length) fail(`${archiveIgnore} must retain the private-key-safe *.pem archive rule`);
|
||||||
|
if (certificateAllowlist <= Math.max(...pemRules)) {
|
||||||
|
fail(`${archiveIgnore} must later allowlist !${certificatePath} for the public OTA certificate`);
|
||||||
|
}
|
||||||
|
if (!file(certificatePath).trim()) fail(`${certificatePath} is empty`);
|
||||||
|
|
||||||
|
return { publicVersion, iosBuild: appBuild };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLocalEas(environment = process.env) {
|
||||||
|
const pluginPath = environment.EAS_LOCAL_BUILD_PLUGIN_PATH;
|
||||||
|
if (!pluginPath) fail('EAS_LOCAL_BUILD_PLUGIN_PATH must name the matching local-build plugin executable bin/run');
|
||||||
|
if (!pluginPath.endsWith('/bin/run') || !existsSync(pluginPath) || !statSync(pluginPath).isFile()) {
|
||||||
|
fail('EAS_LOCAL_BUILD_PLUGIN_PATH must point to an existing executable bin/run, not a package directory');
|
||||||
|
}
|
||||||
|
if (!statSync(pluginPath).mode.toString(8).match(/[1357][0-7][0-7]$/)) fail('EAS_LOCAL_BUILD_PLUGIN_PATH bin/run is not executable');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateFreeDisk(root = repoRoot, readStats = statfsSync) {
|
||||||
|
const stats = readStats(root);
|
||||||
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
||||||
|
if (freeBytes < minimumFreeBuildBytes) {
|
||||||
|
fail(`need at least 20 GiB free before a local build; found ${(freeBytes / 1024 ** 3).toFixed(1)} GiB`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function main(args = process.argv.slice(2), environment = process.env) {
|
||||||
|
if (args.length > 1 || (args.length === 1 && args[0] !== '--local-eas')) {
|
||||||
|
fail('usage: node scripts/release-preflight.mjs [--local-eas]');
|
||||||
|
}
|
||||||
|
const result = validateReleaseSources();
|
||||||
|
validateFreeDisk();
|
||||||
|
if (args.includes('--local-eas')) validateLocalEas(environment);
|
||||||
|
console.log(`Mobile release source preflight passed for v${result.publicVersion}, iOS build ${result.iosBuild}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] === new URL(import.meta.url).pathname) main();
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { validateFreeDisk, validateLocalEas, validateReleaseSources } from './release-preflight.mjs';
|
||||||
|
|
||||||
|
const repoRoot = join(import.meta.dirname, '..', '..', '..');
|
||||||
|
|
||||||
|
function fixture() {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'sauci-release-preflight-'));
|
||||||
|
cpSync(join(repoRoot, 'apps', 'mobile'), join(root, 'apps', 'mobile'), { recursive: true });
|
||||||
|
cpSync(join(repoRoot, '.gitignore'), join(root, '.gitignore'));
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(root, relative, from, to) {
|
||||||
|
const file = join(root, relative);
|
||||||
|
writeFileSync(file, readFileSync(file, 'utf8').replace(from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(root) {
|
||||||
|
return validateReleaseSources({ root, isTracked: () => true });
|
||||||
|
}
|
||||||
|
|
||||||
|
test('accepts current checked-in mobile release sources', () => {
|
||||||
|
const root = fixture();
|
||||||
|
try {
|
||||||
|
const result = validate(root);
|
||||||
|
assert.match(result.publicVersion, /^\d+\.\d+\.\d+$/);
|
||||||
|
assert.match(result.iosBuild, /^\d+$/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a widget Xcode build setting that overrides its plist', () => {
|
||||||
|
const root = fixture();
|
||||||
|
try {
|
||||||
|
const { iosBuild } = validate(root);
|
||||||
|
const project = 'apps/mobile/ios/Sauci.xcodeproj/project.pbxproj';
|
||||||
|
const source = readFileSync(join(root, project), 'utf8');
|
||||||
|
const expected = `CURRENT_PROJECT_VERSION = ${iosBuild};`;
|
||||||
|
const replacement = `CURRENT_PROJECT_VERSION = ${Number(iosBuild) + 1};`;
|
||||||
|
assert.ok(source.includes(expected));
|
||||||
|
replace(root, project, expected, replacement);
|
||||||
|
assert.ok(readFileSync(join(root, project), 'utf8').includes(replacement));
|
||||||
|
assert.throws(() => validate(root), /iOS app, widget, and effective widget Xcode build numbers disagree/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects an OTA certificate without its root archive allowlist', () => {
|
||||||
|
const root = fixture();
|
||||||
|
try {
|
||||||
|
const archiveRules = readFileSync(join(root, '.gitignore'), 'utf8');
|
||||||
|
assert.ok(archiveRules.includes('!apps/mobile/certs/certificate.pem'));
|
||||||
|
replace(root, '.gitignore', '!apps/mobile/certs/certificate.pem', '');
|
||||||
|
assert.ok(!readFileSync(join(root, '.gitignore'), 'utf8').includes('!apps/mobile/certs/certificate.pem'));
|
||||||
|
assert.throws(
|
||||||
|
() => validate(root),
|
||||||
|
/must later allowlist !apps\/mobile\/certs\/certificate\.pem/,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects an Android manifest that lost the media permission removal (expo prebuild regression)', () => {
|
||||||
|
const root = fixture();
|
||||||
|
try {
|
||||||
|
const manifest = 'apps/mobile/android/app/src/main/AndroidManifest.xml';
|
||||||
|
replace(
|
||||||
|
root,
|
||||||
|
manifest,
|
||||||
|
'<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>',
|
||||||
|
'<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>',
|
||||||
|
);
|
||||||
|
assert.throws(() => validate(root), /READ_MEDIA_IMAGES \(expo prebuild regresses this\)/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects an iOS Info.plist that regained dev-launcher-only keys (expo prebuild regression)', () => {
|
||||||
|
const root = fixture();
|
||||||
|
try {
|
||||||
|
const infoPlist = 'apps/mobile/ios/Sauci/Info.plist';
|
||||||
|
replace(
|
||||||
|
root,
|
||||||
|
infoPlist,
|
||||||
|
'</dict>\n</plist>',
|
||||||
|
'<key>NSBonjourServices</key>\n<array><string>_expo._tcp</string></array>\n</dict>\n</plist>',
|
||||||
|
);
|
||||||
|
assert.throws(() => validate(root), /dev-launcher-only keys reintroduced by expo prebuild: NSBonjourServices/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requires the local EAS plugin executable rather than its package directory', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'sauci-eas-plugin-'));
|
||||||
|
const executable = join(root, 'bin-run');
|
||||||
|
try {
|
||||||
|
writeFileSync(executable, '#!/bin/sh\n');
|
||||||
|
chmodSync(executable, 0o755);
|
||||||
|
assert.throws(
|
||||||
|
() => validateLocalEas({ EAS_LOCAL_BUILD_PLUGIN_PATH: root }),
|
||||||
|
/executable bin\/run, not a package directory/,
|
||||||
|
);
|
||||||
|
const bin = join(root, 'bin');
|
||||||
|
mkdirSync(bin);
|
||||||
|
const run = join(bin, 'run');
|
||||||
|
cpSync(executable, run);
|
||||||
|
chmodSync(run, 0o755);
|
||||||
|
validateLocalEas({ EAS_LOCAL_BUILD_PLUGIN_PATH: run });
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requires enough disk before an expensive local build', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => validateFreeDisk('/fixture', () => ({ bavail: 19, bsize: 1024 ** 3 })),
|
||||||
|
/need at least 20 GiB free/,
|
||||||
|
);
|
||||||
|
validateFreeDisk('/fixture', () => ({ bavail: 20, bsize: 1024 ** 3 }));
|
||||||
|
});
|
||||||
@@ -1,31 +1,43 @@
|
|||||||
import { act, renderHook } from '@testing-library/react-native';
|
import { act, renderHook } from '@testing-library/react-native';
|
||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
import { Audio } from 'expo-av';
|
import { AudioModule, requestRecordingPermissionsAsync, setAudioModeAsync } from 'expo-audio';
|
||||||
|
|
||||||
import { useAudioRecorder } from '@/hooks/useAudioRecorder';
|
import { useAudioRecorder } from '@/hooks/useAudioRecorder';
|
||||||
|
|
||||||
|
// eslint-plugin-import's namespace rule cannot statically resolve the
|
||||||
|
// `AudioRecorder` class exposed on the `AudioModule` native module instance.
|
||||||
|
// Alias it once here so the mock constructor is only dereferenced this way
|
||||||
|
// in a single, deliberately suppressed spot.
|
||||||
|
// eslint-disable-next-line import/namespace
|
||||||
|
const AudioRecorderMock = AudioModule.AudioRecorder as jest.Mock;
|
||||||
|
|
||||||
const makeRecording = (overrides: Record<string, unknown> = {}) => ({
|
const makeRecording = (overrides: Record<string, unknown> = {}) => ({
|
||||||
getStatusAsync: jest.fn(async () => ({ isRecording: true, durationMillis: 2400 })),
|
getStatus: jest.fn(() => ({ isRecording: true, durationMillis: 2400, canRecord: true, url: null })),
|
||||||
stopAndUnloadAsync: jest.fn(async () => undefined),
|
prepareToRecordAsync: jest.fn(async () => undefined),
|
||||||
getURI: jest.fn(() => 'file://recording.m4a'),
|
record: jest.fn(),
|
||||||
setOnRecordingStatusUpdate: jest.fn(),
|
stop: jest.fn(async () => undefined),
|
||||||
setProgressUpdateInterval: jest.fn(),
|
uri: 'file://recording.m4a',
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useAudioRecorder', () => {
|
describe('useAudioRecorder', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
jest.spyOn(Alert, 'alert').mockImplementation(() => undefined);
|
jest.spyOn(Alert, 'alert').mockImplementation(() => undefined);
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
(Audio.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted' });
|
(requestRecordingPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted' });
|
||||||
(Audio.setAudioModeAsync as jest.Mock).mockResolvedValue(undefined);
|
(setAudioModeAsync as jest.Mock).mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => jest.restoreAllMocks());
|
afterEach(() => {
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
jest.useRealTimers();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('alerts and refuses to start without microphone permission', async () => {
|
it('alerts and refuses to start without microphone permission', async () => {
|
||||||
(Audio.requestPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied' });
|
(requestRecordingPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied' });
|
||||||
const { result } = renderHook(() => useAudioRecorder());
|
const { result } = renderHook(() => useAudioRecorder());
|
||||||
|
|
||||||
let started = true;
|
let started = true;
|
||||||
@@ -36,21 +48,22 @@ describe('useAudioRecorder', () => {
|
|||||||
'Microphone Permission',
|
'Microphone Permission',
|
||||||
'Please allow microphone access to record audio.'
|
'Please allow microphone access to record audio.'
|
||||||
);
|
);
|
||||||
expect(Audio.Recording.createAsync).not.toHaveBeenCalled();
|
expect(AudioRecorderMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('starts, reports duration, and returns the completed recording', async () => {
|
it('starts, reports duration, and returns the completed recording', async () => {
|
||||||
const recording = makeRecording();
|
const recording = makeRecording();
|
||||||
(Audio.Recording.createAsync as jest.Mock).mockResolvedValue({ recording });
|
AudioRecorderMock.mockImplementation(() => recording);
|
||||||
const onComplete = jest.fn();
|
const onComplete = jest.fn();
|
||||||
const { result } = renderHook(() => useAudioRecorder({ onRecordingComplete: onComplete }));
|
const { result } = renderHook(() => useAudioRecorder({ onRecordingComplete: onComplete }));
|
||||||
|
|
||||||
await act(async () => { expect(await result.current.startRecording()).toBe(true); });
|
await act(async () => { expect(await result.current.startRecording()).toBe(true); });
|
||||||
expect(result.current.state).toBe('recording');
|
expect(result.current.state).toBe('recording');
|
||||||
expect(recording.setProgressUpdateInterval).toHaveBeenCalledWith(100);
|
expect(recording.prepareToRecordAsync).toHaveBeenCalled();
|
||||||
|
expect(recording.record).toHaveBeenCalled();
|
||||||
|
|
||||||
const statusCallback = recording.setOnRecordingStatusUpdate.mock.calls[0][0];
|
// Duration is reported by polling the recorder status, not by a push event.
|
||||||
act(() => statusCallback({ isRecording: true, durationMillis: 2400 }));
|
await act(async () => { jest.advanceTimersByTime(100); });
|
||||||
expect(result.current.durationSeconds).toBe(2);
|
expect(result.current.durationSeconds).toBe(2);
|
||||||
|
|
||||||
let completed: unknown;
|
let completed: unknown;
|
||||||
@@ -66,50 +79,55 @@ describe('useAudioRecorder', () => {
|
|||||||
|
|
||||||
it('auto-stops when the configured duration is reached', async () => {
|
it('auto-stops when the configured duration is reached', async () => {
|
||||||
const recording = makeRecording({
|
const recording = makeRecording({
|
||||||
getStatusAsync: jest.fn(async () => ({ isRecording: true, durationMillis: 1000 })),
|
getStatus: jest.fn(() => ({ isRecording: true, durationMillis: 1000, canRecord: true, url: null })),
|
||||||
});
|
});
|
||||||
(Audio.Recording.createAsync as jest.Mock).mockResolvedValue({ recording });
|
AudioRecorderMock.mockImplementation(() => recording);
|
||||||
const onComplete = jest.fn();
|
const onComplete = jest.fn();
|
||||||
const { result } = renderHook(() => useAudioRecorder({ maxDurationSeconds: 1, onRecordingComplete: onComplete }));
|
const { result } = renderHook(() => useAudioRecorder({ maxDurationSeconds: 1, onRecordingComplete: onComplete }));
|
||||||
|
|
||||||
await act(async () => { await result.current.startRecording(); });
|
await act(async () => { await result.current.startRecording(); });
|
||||||
const statusCallback = recording.setOnRecordingStatusUpdate.mock.calls[0][0];
|
await act(async () => { jest.advanceTimersByTime(100); });
|
||||||
await act(async () => { statusCallback({ isRecording: true, durationMillis: 1000 }); });
|
// Flush the microtasks queued by the auto-stop call.
|
||||||
|
await act(async () => { await Promise.resolve(); });
|
||||||
|
|
||||||
expect(recording.stopAndUnloadAsync).toHaveBeenCalled();
|
expect(recording.stop).toHaveBeenCalled();
|
||||||
expect(onComplete).toHaveBeenCalledWith('file://recording.m4a', 1);
|
expect(onComplete).toHaveBeenCalledWith('file://recording.m4a', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets an active recording and cleans it up on unmount', async () => {
|
it('resets an active recording and cleans it up on unmount', async () => {
|
||||||
const first = makeRecording();
|
const first = makeRecording();
|
||||||
const second = makeRecording();
|
const second = makeRecording();
|
||||||
(Audio.Recording.createAsync as jest.Mock)
|
AudioRecorderMock
|
||||||
.mockResolvedValueOnce({ recording: first })
|
.mockImplementationOnce(() => first)
|
||||||
.mockResolvedValueOnce({ recording: second });
|
.mockImplementationOnce(() => second);
|
||||||
const view = renderHook(() => useAudioRecorder());
|
const view = renderHook(() => useAudioRecorder());
|
||||||
|
|
||||||
await act(async () => { await view.result.current.startRecording(); });
|
await act(async () => { await view.result.current.startRecording(); });
|
||||||
act(() => view.result.current.resetRecording());
|
act(() => view.result.current.resetRecording());
|
||||||
expect(first.stopAndUnloadAsync).toHaveBeenCalled();
|
expect(first.stop).toHaveBeenCalled();
|
||||||
expect(view.result.current.state).toBe('idle');
|
expect(view.result.current.state).toBe('idle');
|
||||||
|
|
||||||
await act(async () => { await view.result.current.startRecording(); });
|
await act(async () => { await view.result.current.startRecording(); });
|
||||||
view.unmount();
|
view.unmount();
|
||||||
expect(second.stopAndUnloadAsync).toHaveBeenCalled();
|
expect(second.stop).toHaveBeenCalled();
|
||||||
expect(Audio.setAudioModeAsync).toHaveBeenLastCalledWith({
|
expect(setAudioModeAsync).toHaveBeenLastCalledWith({
|
||||||
allowsRecordingIOS: false,
|
allowsRecording: false,
|
||||||
playsInSilentModeIOS: true,
|
playsInSilentMode: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recovers when starting or stopping throws', async () => {
|
it('recovers when starting or stopping throws', async () => {
|
||||||
(Audio.Recording.createAsync as jest.Mock).mockRejectedValueOnce(new Error('native unavailable'));
|
AudioRecorderMock.mockImplementationOnce(() => {
|
||||||
|
throw new Error('native unavailable');
|
||||||
|
});
|
||||||
const first = renderHook(() => useAudioRecorder());
|
const first = renderHook(() => useAudioRecorder());
|
||||||
await act(async () => { expect(await first.result.current.startRecording()).toBe(false); });
|
await act(async () => { expect(await first.result.current.startRecording()).toBe(false); });
|
||||||
expect(first.result.current.state).toBe('idle');
|
expect(first.result.current.state).toBe('idle');
|
||||||
|
|
||||||
const recording = makeRecording({ getStatusAsync: jest.fn(async () => { throw new Error('lost'); }) });
|
const recording = makeRecording({
|
||||||
(Audio.Recording.createAsync as jest.Mock).mockResolvedValueOnce({ recording });
|
getStatus: jest.fn(() => { throw new Error('lost'); }),
|
||||||
|
});
|
||||||
|
AudioRecorderMock.mockImplementationOnce(() => recording);
|
||||||
const second = renderHook(() => useAudioRecorder());
|
const second = renderHook(() => useAudioRecorder());
|
||||||
await act(async () => { await second.result.current.startRecording(); });
|
await act(async () => { await second.result.current.startRecording(); });
|
||||||
await act(async () => { expect(await second.result.current.stopRecording()).toBeNull(); });
|
await act(async () => { expect(await second.result.current.stopRecording()).toBeNull(); });
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { act, renderHook } from '@testing-library/react-native';
|
import { act, renderHook } from '@testing-library/react-native';
|
||||||
import { ActionSheetIOS, Alert, Platform } from 'react-native';
|
import { ActionSheetIOS, Alert, Linking, Platform } from 'react-native';
|
||||||
import * as FileSystem from 'expo-file-system/legacy';
|
import * as FileSystem from 'expo-file-system/legacy';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { useAvatarPicker } from '@/hooks/useAvatarPicker';
|
import { useAvatarPicker } from '@/hooks/useAvatarPicker';
|
||||||
|
|
||||||
|
jest.mock('@/lib/mediaApi', () => ({
|
||||||
|
uploadMedia: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('expo-file-system/legacy', () => ({
|
jest.mock('expo-file-system/legacy', () => ({
|
||||||
getInfoAsync: jest.fn(),
|
getInfoAsync: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('expo-image-picker', () => ({
|
jest.mock('expo-image-picker', () => ({
|
||||||
|
getCameraPermissionsAsync: jest.fn(),
|
||||||
requestCameraPermissionsAsync: jest.fn(),
|
requestCameraPermissionsAsync: jest.fn(),
|
||||||
requestMediaLibraryPermissionsAsync: jest.fn(),
|
requestMediaLibraryPermissionsAsync: jest.fn(),
|
||||||
launchCameraAsync: jest.fn(),
|
launchCameraAsync: jest.fn(),
|
||||||
@@ -23,7 +28,10 @@ describe('useAvatarPicker', () => {
|
|||||||
Object.defineProperty(Platform, 'OS', { configurable: true, value: 'android' });
|
Object.defineProperty(Platform, 'OS', { configurable: true, value: 'android' });
|
||||||
jest.spyOn(Alert, 'alert').mockImplementation(() => undefined);
|
jest.spyOn(Alert, 'alert').mockImplementation(() => undefined);
|
||||||
jest.spyOn(ActionSheetIOS, 'showActionSheetWithOptions').mockImplementation(() => undefined);
|
jest.spyOn(ActionSheetIOS, 'showActionSheetWithOptions').mockImplementation(() => undefined);
|
||||||
|
jest.spyOn(Linking, 'openSettings').mockResolvedValue(undefined);
|
||||||
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValue({ canceled: true, assets: [] });
|
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValue({ canceled: true, assets: [] });
|
||||||
|
(ImagePicker.getCameraPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'undetermined', canAskAgain: true });
|
||||||
|
(ImagePicker.requestCameraPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted' });
|
||||||
(FileSystem.getInfoAsync as jest.Mock).mockResolvedValue({ exists: true });
|
(FileSystem.getInfoAsync as jest.Mock).mockResolvedValue({ exists: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,4 +72,41 @@ describe('useAvatarPicker', () => {
|
|||||||
expect(ImagePicker.requestMediaLibraryPermissionsAsync).toHaveBeenCalledTimes(1);
|
expect(ImagePicker.requestMediaLibraryPermissionsAsync).toHaveBeenCalledTimes(1);
|
||||||
expect(ImagePicker.launchImageLibraryAsync).toHaveBeenCalledTimes(1);
|
expect(ImagePicker.launchImageLibraryAsync).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not show a custom alert immediately after a camera permission denial', async () => {
|
||||||
|
const { result } = renderHook(() => useAvatarPicker());
|
||||||
|
(ImagePicker.requestCameraPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied', canAskAgain: false });
|
||||||
|
|
||||||
|
act(() => result.current.showPicker());
|
||||||
|
const takePhoto = (Alert.alert as jest.Mock).mock.calls[0][2].find(
|
||||||
|
(option: { text: string }) => option.text === 'Take Photo'
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => takePhoto.onPress());
|
||||||
|
|
||||||
|
expect(Alert.alert).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ImagePicker.launchCameraAsync).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('offers Settings only for a later camera attempt that iOS cannot prompt again', async () => {
|
||||||
|
(ImagePicker.getCameraPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied', canAskAgain: false });
|
||||||
|
const { result } = renderHook(() => useAvatarPicker());
|
||||||
|
|
||||||
|
act(() => result.current.showPicker());
|
||||||
|
const takePhoto = (Alert.alert as jest.Mock).mock.calls[0][2].find(
|
||||||
|
(option: { text: string }) => option.text === 'Take Photo'
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => takePhoto.onPress());
|
||||||
|
|
||||||
|
const settingsAlert = (Alert.alert as jest.Mock).mock.calls[1];
|
||||||
|
expect(settingsAlert[0]).toBe('Camera Access');
|
||||||
|
expect(settingsAlert[2]).toEqual([
|
||||||
|
{ text: 'Not Now', style: 'cancel' },
|
||||||
|
expect.objectContaining({ text: 'Open Settings' }),
|
||||||
|
]);
|
||||||
|
await act(async () => settingsAlert[2][1].onPress());
|
||||||
|
expect(Linking.openSettings).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ImagePicker.requestCameraPermissionsAsync).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiError, apiRequest } from "@/lib/apiClient";
|
import { ApiError, apiRequest, apiRequestWithAccessToken } from "@/lib/apiClient";
|
||||||
import { authClient } from "@/lib/authClient";
|
import { authClient } from "@/lib/authClient";
|
||||||
|
|
||||||
function response(status: number, body?: unknown) {
|
function response(status: number, body?: unknown) {
|
||||||
@@ -40,6 +40,17 @@ describe("apiClient", () => {
|
|||||||
expect(fetchMock).not.toHaveBeenCalled();
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses an explicitly captured bearer without reading a mutable auth session", async () => {
|
||||||
|
fetchMock.mockResolvedValue(response(200, { profile: { id: "user-1" } }));
|
||||||
|
(authClient.auth.getSession as jest.Mock).mockClear();
|
||||||
|
|
||||||
|
await apiRequestWithAccessToken("/v1/me", "captured-token");
|
||||||
|
|
||||||
|
const headers = fetchMock.mock.calls[0][1].headers as Headers;
|
||||||
|
expect(headers.get("Authorization")).toBe("Bearer captured-token");
|
||||||
|
expect(authClient.auth.getSession).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves API status and error details", async () => {
|
it("preserves API status and error details", async () => {
|
||||||
fetchMock.mockResolvedValue(response(403, {
|
fetchMock.mockResolvedValue(response(403, {
|
||||||
error: { message: "Couple access denied", code: "FORBIDDEN" },
|
error: { message: "Couple access denied", code: "FORBIDDEN" },
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { formatAppleProfileName } from '@/lib/appleProfileName';
|
||||||
|
|
||||||
|
describe('formatAppleProfileName', () => {
|
||||||
|
it('combines Apple name parts into the profile display name', () => {
|
||||||
|
expect(formatAppleProfileName({
|
||||||
|
givenName: ' Ada ',
|
||||||
|
middleName: ' Lovelace',
|
||||||
|
familyName: ' Byron ',
|
||||||
|
})).toBe('Ada Lovelace Byron');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when Apple has not supplied a usable name', () => {
|
||||||
|
expect(formatAppleProfileName({ givenName: ' \n\t ' })).toBeNull();
|
||||||
|
expect(formatAppleProfileName(null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes control characters and stays within the profile contract limit', () => {
|
||||||
|
expect(formatAppleProfileName({ givenName: 'Ada\u0000 Lovelace' })).toBe('Ada Lovelace');
|
||||||
|
expect(formatAppleProfileName({ givenName: 'a'.repeat(101) })).toHaveLength(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { persistAppleProfileName } from '@/lib/appleProfileNamePersistence';
|
||||||
|
|
||||||
|
describe('persistAppleProfileName', () => {
|
||||||
|
const session = { userId: 'apple-user', accessToken: 'apple-token' };
|
||||||
|
const getProfile = jest.fn();
|
||||||
|
const updateProfileName = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes a supplied name only after a matching blank profile is read with the captured bearer', async () => {
|
||||||
|
getProfile.mockResolvedValue({ profile: { id: 'apple-user', name: null } });
|
||||||
|
updateProfileName.mockResolvedValue({ updated: true });
|
||||||
|
|
||||||
|
await expect(persistAppleProfileName(session, 'Ada Lovelace', { getProfile, updateProfileName }))
|
||||||
|
.resolves.toBe('updated');
|
||||||
|
expect(getProfile).toHaveBeenCalledWith('apple-token');
|
||||||
|
expect(updateProfileName).toHaveBeenCalledWith('apple-token', 'Ada Lovelace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves a returning user nickname', async () => {
|
||||||
|
getProfile.mockResolvedValue({ profile: { id: 'apple-user', name: 'Ada B' } });
|
||||||
|
|
||||||
|
await expect(persistAppleProfileName(session, 'Ada Lovelace', { getProfile, updateProfileName }))
|
||||||
|
.resolves.toBe('existing');
|
||||||
|
expect(updateProfileName).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails closed on a subject mismatch before any profile write', async () => {
|
||||||
|
getProfile.mockResolvedValue({ profile: { id: 'different-user', name: null } });
|
||||||
|
|
||||||
|
await expect(persistAppleProfileName(session, 'Ada Lovelace', { getProfile, updateProfileName }))
|
||||||
|
.rejects.toThrow('subject did not match');
|
||||||
|
expect(updateProfileName).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagates API failure so the caller can clear its pending onboarding name', async () => {
|
||||||
|
getProfile.mockRejectedValue(new Error('offline'));
|
||||||
|
|
||||||
|
await expect(persistAppleProfileName(session, 'Ada Lovelace', { getProfile, updateProfileName }))
|
||||||
|
.rejects.toThrow('offline');
|
||||||
|
expect(updateProfileName).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
|
import {
|
||||||
|
hasSeenMatchesTutorial,
|
||||||
|
markMatchesTutorialSeen,
|
||||||
|
resetMatchesTutorial,
|
||||||
|
} from "@/lib/matchesTutorialSeen";
|
||||||
|
|
||||||
|
jest.mock("expo-secure-store", () => ({
|
||||||
|
getItemAsync: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("matchesTutorialSeen", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await AsyncStorage.clear();
|
||||||
|
jest.mocked(SecureStore.getItemAsync).mockReset();
|
||||||
|
jest.mocked(SecureStore.getItemAsync).mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists a dismissal for the signed-in account", async () => {
|
||||||
|
await markMatchesTutorialSeen("user-1");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not dismiss the tutorial for another account on the same device", async () => {
|
||||||
|
await markMatchesTutorialSeen("user-1");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-2")).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates an existing legacy dismissal into durable account storage", async () => {
|
||||||
|
jest.mocked(SecureStore.getItemAsync).mockResolvedValue("true");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(true);
|
||||||
|
await expect(AsyncStorage.getItem("matches_tutorial_seen_user-1")).resolves.toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets the development reset show the tutorial for the current account again", async () => {
|
||||||
|
await markMatchesTutorialSeen("user-1");
|
||||||
|
await resetMatchesTutorial("user-1");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
jest.mock('expo-observe', () => ({
|
||||||
|
Observe: {
|
||||||
|
logEvent: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { Observe } = require('expo-observe');
|
||||||
|
const logEventMock = Observe.logEvent as jest.Mock;
|
||||||
|
|
||||||
|
type ErrorHandler = (error: unknown, isFatal: boolean) => void;
|
||||||
|
|
||||||
|
function withFreshObserveModule<T>(run: (mod: typeof import('@/lib/observe')) => T): T {
|
||||||
|
let result!: T;
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const mod = require('@/lib/observe') as typeof import('@/lib/observe');
|
||||||
|
result = run(mod);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installFakeErrorUtils(previousHandler: jest.Mock) {
|
||||||
|
let installedHandler: ErrorHandler = () => undefined;
|
||||||
|
const globalWithErrorUtils = global as unknown as {
|
||||||
|
ErrorUtils: { getGlobalHandler: () => unknown; setGlobalHandler: (fn: unknown) => void };
|
||||||
|
};
|
||||||
|
globalWithErrorUtils.ErrorUtils = {
|
||||||
|
getGlobalHandler: () => previousHandler,
|
||||||
|
setGlobalHandler: (fn) => {
|
||||||
|
installedHandler = fn as ErrorHandler;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return () => installedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('observe', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs app_started only once per module instance', () => {
|
||||||
|
withFreshObserveModule(({ logAppStarted }) => {
|
||||||
|
logAppStarted();
|
||||||
|
logAppStarted();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(logEventMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logEventMock).toHaveBeenCalledWith('app_started');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when Observe.logEvent fails', () => {
|
||||||
|
logEventMock.mockImplementationOnce(() => {
|
||||||
|
throw new Error('native module unavailable');
|
||||||
|
});
|
||||||
|
|
||||||
|
withFreshObserveModule(({ logAppStarted }) => {
|
||||||
|
expect(() => logAppStarted()).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps the previous ErrorUtils handler, logging xprem_js_crash and delegating', () => {
|
||||||
|
const previousHandler = jest.fn();
|
||||||
|
const getInstalledHandler = installFakeErrorUtils(previousHandler);
|
||||||
|
|
||||||
|
withFreshObserveModule(({ installObserveGlobalErrorHandler }) => {
|
||||||
|
installObserveGlobalErrorHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error('boom');
|
||||||
|
getInstalledHandler()(error, true);
|
||||||
|
|
||||||
|
expect(logEventMock).toHaveBeenCalledWith(
|
||||||
|
'xprem_js_crash',
|
||||||
|
expect.objectContaining({
|
||||||
|
severity: 'fatal',
|
||||||
|
body: 'boom',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(previousHandler).toHaveBeenCalledWith(error, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not log xprem_js_crash for non-fatal errors but still delegates', () => {
|
||||||
|
const previousHandler = jest.fn();
|
||||||
|
const getInstalledHandler = installFakeErrorUtils(previousHandler);
|
||||||
|
|
||||||
|
withFreshObserveModule(({ installObserveGlobalErrorHandler }) => {
|
||||||
|
installObserveGlobalErrorHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error('handled');
|
||||||
|
getInstalledHandler()(error, false);
|
||||||
|
|
||||||
|
expect(logEventMock).not.toHaveBeenCalled();
|
||||||
|
expect(previousHandler).toHaveBeenCalledWith(error, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still calls the previous handler when Observe.logEvent throws', () => {
|
||||||
|
const previousHandler = jest.fn();
|
||||||
|
const getInstalledHandler = installFakeErrorUtils(previousHandler);
|
||||||
|
logEventMock.mockImplementationOnce(() => {
|
||||||
|
throw new Error('native module unavailable');
|
||||||
|
});
|
||||||
|
|
||||||
|
withFreshObserveModule(({ installObserveGlobalErrorHandler }) => {
|
||||||
|
installObserveGlobalErrorHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
getInstalledHandler()(new Error('boom'), true);
|
||||||
|
|
||||||
|
expect(previousHandler).toHaveBeenCalledWith(expect.any(Error), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { activeOnboardingStage, initialOnboardingStage, previousOnboardingStage } from '@/lib/onboardingStages';
|
||||||
|
|
||||||
|
describe('initialOnboardingStage', () => {
|
||||||
|
it('starts at gender when a profile already has an Apple-supplied name', () => {
|
||||||
|
expect(initialOnboardingStage('Ada Lovelace')).toBe('gender');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts at name as a fallback when no name is available', () => {
|
||||||
|
expect(initialOnboardingStage(' ')).toBe('name');
|
||||||
|
expect(initialOnboardingStage(null)).toBe('name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not provide a back destination before the first reachable stage', () => {
|
||||||
|
expect(previousOnboardingStage('name', false)).toBeNull();
|
||||||
|
expect(previousOnboardingStage('gender', true)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows backwards navigation only through reachable stages', () => {
|
||||||
|
expect(previousOnboardingStage('gender', false)).toBe('name');
|
||||||
|
expect(previousOnboardingStage('purpose', true)).toBe('gender');
|
||||||
|
expect(previousOnboardingStage('notifications', false)).toBe('purpose');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps first-auth onboarding out of the name screen only while its subject-keyed name is pending', () => {
|
||||||
|
expect(activeOnboardingStage('name', null, 'Ada Lovelace')).toBe('gender');
|
||||||
|
expect(activeOnboardingStage('name', null, undefined)).toBe('name');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { apiClient } from '../../lib/apiClient';
|
import { apiClient, apiRequestWithAccessToken } from '../../lib/apiClient';
|
||||||
import { profileSettingsApi } from '../../lib/profileSettingsApi';
|
import { profileSettingsApi } from '../../lib/profileSettingsApi';
|
||||||
|
|
||||||
jest.mock('../../lib/apiClient', () => ({
|
jest.mock('../../lib/apiClient', () => ({
|
||||||
@@ -7,6 +7,7 @@ jest.mock('../../lib/apiClient', () => ({
|
|||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
patch: jest.fn(),
|
patch: jest.fn(),
|
||||||
},
|
},
|
||||||
|
apiRequestWithAccessToken: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('profileSettingsApi', () => {
|
describe('profileSettingsApi', () => {
|
||||||
@@ -27,4 +28,14 @@ describe('profileSettingsApi', () => {
|
|||||||
type: 'bug', title: 'Title', description: 'Details',
|
type: 'bug', title: 'Title', description: 'Details',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can bind a profile update to a captured authentication bearer', async () => {
|
||||||
|
await profileSettingsApi.updateProfileWithAccessToken('captured-token', { name: 'Ada' });
|
||||||
|
|
||||||
|
expect(apiRequestWithAccessToken).toHaveBeenCalledWith(
|
||||||
|
'/v1/me/profile',
|
||||||
|
'captured-token',
|
||||||
|
{ method: 'PATCH', body: { name: 'Ada' } },
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -30,10 +30,14 @@ const profile = {
|
|||||||
describe("authStore", () => {
|
describe("authStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
(authClient.auth.getSession as jest.Mock).mockResolvedValue({
|
||||||
|
data: { session: { user: { id: "me" }, access_token: "token-me" } },
|
||||||
|
});
|
||||||
// fetchUser always fetches couple state now, since it also carries the
|
// fetchUser always fetches couple state now, since it also carries the
|
||||||
// sealed answer count for a user who has no couple yet. Individual tests
|
// sealed answer count for a user who has no couple yet. Individual tests
|
||||||
// override this when they care about the couple/partner shape.
|
// override this when they care about the couple/partner shape.
|
||||||
jest.spyOn(coupleApi, "getState").mockResolvedValue({ couple: null, partner: null, sealed_count: 0 });
|
jest.spyOn(coupleApi, "getState").mockResolvedValue({ couple: null, partner: null, sealed_count: 0 });
|
||||||
|
jest.spyOn(coupleApi, "getStateWithAccessToken").mockResolvedValue({ couple: null, partner: null, sealed_count: 0 });
|
||||||
useAuthStore.setState({
|
useAuthStore.setState({
|
||||||
user: null,
|
user: null,
|
||||||
couple: null,
|
couple: null,
|
||||||
@@ -42,6 +46,7 @@ describe("authStore", () => {
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isAnonymous: false,
|
isAnonymous: false,
|
||||||
|
pendingAppleDisplayName: null,
|
||||||
} as any);
|
} as any);
|
||||||
useMatchStore.setState({ matches: [{ id: "m1" }], newMatchesCount: 1 } as any);
|
useMatchStore.setState({ matches: [{ id: "m1" }], newMatchesCount: 1 } as any);
|
||||||
usePacksStore.setState({ enabledPackIds: ["p1"] } as any);
|
usePacksStore.setState({ enabledPackIds: ["p1"] } as any);
|
||||||
@@ -64,13 +69,13 @@ describe("authStore", () => {
|
|||||||
|
|
||||||
it("bootstraps and reads the profile through GET /v1/me", async () => {
|
it("bootstraps and reads the profile through GET /v1/me", async () => {
|
||||||
(authClient.auth.getSession as jest.Mock).mockResolvedValueOnce({
|
(authClient.auth.getSession as jest.Mock).mockResolvedValueOnce({
|
||||||
data: { session: { user: { id: "me", is_anonymous: true } } },
|
data: { session: { user: { id: "me", is_anonymous: true }, access_token: "token-me" } },
|
||||||
});
|
});
|
||||||
const getSpy = jest.spyOn(apiClient, "get").mockResolvedValueOnce({ profile });
|
const getSpy = jest.spyOn(apiClient, "getWithAccessToken").mockResolvedValueOnce({ profile });
|
||||||
|
|
||||||
await useAuthStore.getState().fetchUser();
|
await useAuthStore.getState().fetchUser();
|
||||||
|
|
||||||
expect(getSpy).toHaveBeenCalledWith("/v1/me");
|
expect(getSpy).toHaveBeenCalledWith("/v1/me", "token-me");
|
||||||
expect(useAuthStore.getState()).toMatchObject({
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
user: profile,
|
user: profile,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
@@ -114,9 +119,9 @@ describe("authStore", () => {
|
|||||||
it("clears user-scoped stores and hosted auth when the API rejects the session", async () => {
|
it("clears user-scoped stores and hosted auth when the API rejects the session", async () => {
|
||||||
useAuthStore.setState({ sealedCount: 3 } as any);
|
useAuthStore.setState({ sealedCount: 3 } as any);
|
||||||
(authClient.auth.getSession as jest.Mock).mockResolvedValueOnce({
|
(authClient.auth.getSession as jest.Mock).mockResolvedValueOnce({
|
||||||
data: { session: { user: { id: "me" } } },
|
data: { session: { user: { id: "me" }, access_token: "token-me" } },
|
||||||
});
|
});
|
||||||
jest.spyOn(apiClient, "get").mockRejectedValueOnce(new ApiError("Unauthorized", 401));
|
jest.spyOn(apiClient, "getWithAccessToken").mockRejectedValueOnce(new ApiError("Unauthorized", 401));
|
||||||
(authClient.auth.signOut as jest.Mock).mockResolvedValueOnce({ error: null });
|
(authClient.auth.signOut as jest.Mock).mockResolvedValueOnce({ error: null });
|
||||||
|
|
||||||
await useAuthStore.getState().fetchUser();
|
await useAuthStore.getState().fetchUser();
|
||||||
@@ -135,6 +140,123 @@ describe("authStore", () => {
|
|||||||
expect(authClient.auth.signOut).toHaveBeenCalled();
|
expect(authClient.auth.signOut).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not let an older session response overwrite a newer profile", async () => {
|
||||||
|
let currentSession = { user: { id: "user-a" }, access_token: "token-a" };
|
||||||
|
(authClient.auth.getSession as jest.Mock).mockImplementation(async () => ({ data: { session: currentSession } }));
|
||||||
|
|
||||||
|
let resolveA!: (value: { profile: Profile }) => void;
|
||||||
|
let resolveB!: (value: { profile: Profile }) => void;
|
||||||
|
const getSpy = jest.spyOn(apiClient, "getWithAccessToken")
|
||||||
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveA = resolve; }))
|
||||||
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveB = resolve; }));
|
||||||
|
|
||||||
|
const firstFetch = useAuthStore.getState().fetchUser();
|
||||||
|
currentSession = { user: { id: "user-b" }, access_token: "token-b" };
|
||||||
|
const secondFetch = useAuthStore.getState().fetchUser();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
resolveB({ profile: { ...profile, id: "user-b", name: "Newer Name" } as Profile });
|
||||||
|
await secondFetch;
|
||||||
|
resolveA({ profile: { ...profile, id: "user-a", name: "Older Name" } as Profile });
|
||||||
|
await firstFetch;
|
||||||
|
|
||||||
|
expect(getSpy).toHaveBeenNthCalledWith(1, "/v1/me", "token-a");
|
||||||
|
expect(getSpy).toHaveBeenNthCalledWith(2, "/v1/me", "token-b");
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
user: expect.objectContaining({ id: "user-b", name: "Newer Name" }),
|
||||||
|
isAuthenticated: true,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not let an older couple response overwrite a newer session", async () => {
|
||||||
|
let currentSession = { user: { id: "user-a" }, access_token: "token-a" };
|
||||||
|
(authClient.auth.getSession as jest.Mock).mockImplementation(async () => ({ data: { session: currentSession } }));
|
||||||
|
jest.spyOn(apiClient, "getWithAccessToken")
|
||||||
|
.mockResolvedValueOnce({ profile: { ...profile, id: "user-a", name: "Older Name" } })
|
||||||
|
.mockResolvedValueOnce({ profile: { ...profile, id: "user-b", name: "Newer Name" } });
|
||||||
|
|
||||||
|
let resolveCoupleA!: (value: { couple: null; partner: null; sealed_count: number }) => void;
|
||||||
|
jest.spyOn(coupleApi, "getStateWithAccessToken")
|
||||||
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveCoupleA = resolve; }))
|
||||||
|
.mockResolvedValueOnce({ couple: null, partner: null, sealed_count: 7 });
|
||||||
|
|
||||||
|
const firstFetch = useAuthStore.getState().fetchUser();
|
||||||
|
for (let attempt = 0; attempt < 10 && !resolveCoupleA; attempt += 1) {
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
expect(resolveCoupleA).toBeDefined();
|
||||||
|
currentSession = { user: { id: "user-b" }, access_token: "token-b" };
|
||||||
|
const secondFetch = useAuthStore.getState().fetchUser();
|
||||||
|
await secondFetch;
|
||||||
|
resolveCoupleA({ couple: null, partner: null, sealed_count: 1 });
|
||||||
|
await firstFetch;
|
||||||
|
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
user: expect.objectContaining({ id: "user-b", name: "Newer Name" }),
|
||||||
|
sealedCount: 7,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a mismatched API profile without signing out the active session", async () => {
|
||||||
|
useAuthStore.setState({
|
||||||
|
user: profile,
|
||||||
|
couple: { id: "prior-couple", invite_code: "PRIOR123", created_at: profile.created_at },
|
||||||
|
partner: { ...profile, id: "prior-partner", name: "Prior Partner" },
|
||||||
|
sealedCount: 4,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isAnonymous: true,
|
||||||
|
pendingAppleDisplayName: { userId: "me", name: "Prior Name" },
|
||||||
|
} as any);
|
||||||
|
jest.spyOn(apiClient, "getWithAccessToken").mockResolvedValueOnce({
|
||||||
|
profile: { ...profile, id: "different-user", name: "Wrong Subject" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await useAuthStore.getState().fetchUser();
|
||||||
|
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
user: null,
|
||||||
|
couple: null,
|
||||||
|
partner: null,
|
||||||
|
sealedCount: 0,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isAnonymous: false,
|
||||||
|
isLoading: false,
|
||||||
|
pendingAppleDisplayName: null,
|
||||||
|
});
|
||||||
|
expect(useMatchStore.getState().matches).toEqual([]);
|
||||||
|
expect(usePacksStore.getState().enabledPackIds).toEqual([]);
|
||||||
|
expect(useMessageStore.getState().unreadCount).toBe(0);
|
||||||
|
expect(useSubscriptionStore.getState().subscription).toMatchObject({ isProUser: false });
|
||||||
|
expect(authClient.auth.signOut).not.toHaveBeenCalled();
|
||||||
|
expect(coupleApi.getStateWithAccessToken).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not sign out a newer session when an older request is rejected", async () => {
|
||||||
|
let currentSession = { user: { id: "user-a" }, access_token: "token-a" };
|
||||||
|
(authClient.auth.getSession as jest.Mock).mockImplementation(async () => ({ data: { session: currentSession } }));
|
||||||
|
|
||||||
|
let rejectA!: (error: Error) => void;
|
||||||
|
const getSpy = jest.spyOn(apiClient, "getWithAccessToken")
|
||||||
|
.mockImplementationOnce(() => new Promise((_, reject) => { rejectA = reject; }))
|
||||||
|
.mockResolvedValueOnce({ profile: { ...profile, id: "user-b", name: "Newer Name" } });
|
||||||
|
|
||||||
|
const firstFetch = useAuthStore.getState().fetchUser();
|
||||||
|
currentSession = { user: { id: "user-b" }, access_token: "token-b" };
|
||||||
|
const secondFetch = useAuthStore.getState().fetchUser();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
await secondFetch;
|
||||||
|
rejectA(new ApiError("Unauthorized", 401));
|
||||||
|
await firstFetch;
|
||||||
|
|
||||||
|
expect(getSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(authClient.auth.signOut).not.toHaveBeenCalled();
|
||||||
|
expect(useAuthStore.getState().user).toMatchObject({ id: "user-b", name: "Newer Name" });
|
||||||
|
});
|
||||||
|
|
||||||
it("clears sealed answers during an explicit sign-out", async () => {
|
it("clears sealed answers during an explicit sign-out", async () => {
|
||||||
useAuthStore.setState({ user: profile, sealedCount: 3, isAuthenticated: true } as any);
|
useAuthStore.setState({ user: profile, sealedCount: 3, isAuthenticated: true } as any);
|
||||||
|
|
||||||
@@ -158,4 +280,14 @@ describe("authStore", () => {
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps a pending Apple name scoped to its original subject', () => {
|
||||||
|
useAuthStore.getState().setPendingAppleDisplayName({ userId: 'apple-user', name: 'Ada Lovelace' });
|
||||||
|
useAuthStore.getState().clearPendingAppleDisplayName('other-user');
|
||||||
|
|
||||||
|
expect(useAuthStore.getState().pendingAppleDisplayName).toEqual({ userId: 'apple-user', name: 'Ada Lovelace' });
|
||||||
|
|
||||||
|
useAuthStore.getState().clearPendingAppleDisplayName('apple-user');
|
||||||
|
expect(useAuthStore.getState().pendingAppleDisplayName).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -310,7 +310,7 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.textTertiary,
|
color: colors.textTertiary,
|
||||||
},
|
},
|
||||||
lockOverlay: {
|
lockOverlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ export const MatchConfetti: React.FC<Props> = ({ visible, onAnimationComplete })
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
},
|
},
|
||||||
particle: {
|
particle: {
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export const MatchNotificationModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -377,7 +377,7 @@ const styles = StyleSheet.create({
|
|||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
},
|
},
|
||||||
flatBackground: {
|
flatBackground: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: colors.backgroundLight,
|
backgroundColor: colors.backgroundLight,
|
||||||
},
|
},
|
||||||
// Removed blur/gradient styles
|
// Removed blur/gradient styles
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const styles = StyleSheet.create({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||||
borderRadius: radius.lg,
|
borderRadius: radius.lg,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ const styles = StyleSheet.create({
|
|||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
},
|
},
|
||||||
backdrop: {
|
backdrop: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFill,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
},
|
},
|
||||||
keyboardView: {
|
keyboardView: {
|
||||||
|
|||||||