Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63b8808a2a |
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: v1.0.6 exported IPA inspection and reproduced v1.0.7 Xcode build-setting mismatch
|
evidence: build 45 Xcode archive warning plus direct PlistBuddy inspection of the exported IPA
|
||||||
destination: apps/mobile/scripts/release-preflight.mjs and .codex/skills/release-mobile/SKILL.md
|
destination: .codex/skills/release-mobile/SKILL.md
|
||||||
review_after: 2027-02-28
|
review_after: 2027-02-28
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -21,23 +21,19 @@ and every packaged extension `Info.plist` with `PlistBuddy`.
|
|||||||
|
|
||||||
## Cause
|
## Cause
|
||||||
|
|
||||||
The v1.0.6 archive warning described an intermediate state and was not
|
The archive warning described an intermediate state and was not authoritative
|
||||||
authoritative for that exported package. The later v1.0.7 reproduction found a
|
for the exported package. Direct IPA inspection showed both the app and
|
||||||
separate real hazard: `CURRENT_PROJECT_VERSION` in both LiveDrawWidget
|
`LiveDrawWidget.appex` at version 1.0.6 build 45.
|
||||||
configurations can override the widget plist and package an older build number.
|
|
||||||
|
|
||||||
## Remediation
|
## Remediation
|
||||||
|
|
||||||
Before every iOS release, require public-version parity across app config, both
|
For every iOS release, inspect version and build values in the exported app and
|
||||||
plists, and Xcode marketing settings. Require the widget Debug and Release
|
all packaged extensions. Reject or rebuild only when the final IPA values differ;
|
||||||
`CURRENT_PROJECT_VERSION` values to equal both plists. Then inspect the exported
|
do not accept or reject a release from archive warnings alone.
|
||||||
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 v1.0.6 packaged plists. The v1.0.7 build later proved a stale widget Xcode
|
both packaged plists. `codesign --verify --deep --strict` passed for the app, and
|
||||||
setting can survive plist-only review. `release-preflight.mjs` and its node test
|
the verified IPA SHA-256 was
|
||||||
now reject that mismatch before EAS allocation; exported IPA inspection remains
|
`9d4779bddac4647911a3a38e4ee37cf794c2b8691e0746a041b629c62b308e7f`.
|
||||||
the final authority.
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
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, verify, and ship Sauci iOS or Android releases end to end, including store uploads, review submission, and rollout.
|
description: Prepare and verify Sauci iOS or Android releases and store uploads.
|
||||||
---
|
---
|
||||||
|
|
||||||
Read `.codex/skills/release-mobile/SKILL.md` completely and follow it.
|
Read `.codex/skills/release-mobile/SKILL.md` completely and follow it.
|
||||||
@@ -13,13 +13,9 @@ 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. When
|
release notes, local builds, release commits, and one local annotated tag. It
|
||||||
the request also asks to ship, upload, submit, or release to the stores, it
|
does not authorize pushing commits or tags, uploading binaries, submitting a
|
||||||
authorizes the complete store pipeline for the stated platforms after the exact
|
store release, changing signing credentials, or regenerating native projects.
|
||||||
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
|
||||||
|
|
||||||
@@ -43,8 +39,6 @@ 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.
|
||||||
@@ -86,15 +80,6 @@ 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
|
||||||
@@ -103,19 +88,15 @@ 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, signing identity/fingerprint, OTA runtime/channel/certificate,
|
inspectable, and signing identity/fingerprint. Require the embedded public
|
||||||
and SHA-256. Require the embedded public version to equal the requested
|
version to equal the requested version.
|
||||||
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. Quarantine
|
Do not accept a successful EAS exit code as proof of a valid artifact. If either
|
||||||
or remove rejected artifacts so only accepted paths can be offered for upload. If
|
requested platform fails, do not tag a partial release.
|
||||||
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, 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.
|
description: Prepare and verify Sauci iOS or Android releases. Use for versioning, EAS/native builds, Fastlane upload, release notes, or store submission preparation.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mobile release
|
# Mobile release
|
||||||
@@ -9,41 +9,6 @@ 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
|
||||||
@@ -52,65 +17,40 @@ 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.
|
||||||
|
|
||||||
Run `npm run release:preflight -w @sauci/mobile` before allocating an EAS build.
|
Use the package scripts in `apps/mobile/package.json`; they create artifacts only.
|
||||||
It rejects inconsistent public/runtime versions, an iOS widget build-setting
|
Never infer upload or submission authority from build authorization. Store upload
|
||||||
override, and a public OTA certificate that would be omitted from the archive.
|
uses a separately authorized command.
|
||||||
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.
|
|
||||||
|
|
||||||
Classify a failure before rebuilding. Archive, plugin-spawn, package-install, and
|
## Upload authorization gate
|
||||||
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.
|
|
||||||
|
|
||||||
Report provider progress by phase: packaging, native compilation, submission
|
After an artifact passes verification, stop immediately before the external
|
||||||
queue, or store processing. Do not describe every failure as a rebuild.
|
upload and ask the user for confirmation. The request must identify:
|
||||||
|
|
||||||
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: which Play track and rollout
|
- the exact destination and resulting state: Google Play internal-track draft or
|
||||||
status, or App Store Connect version state and whether App Review submission
|
App Store Connect/TestFlight processing;
|
||||||
and automatic release are included.
|
- that the upload does not authorize a public rollout or App Review submission.
|
||||||
|
|
||||||
Once these details match the request, proceed through upload, review
|
For iOS, inspect the current App Store Connect automatic-distribution behavior
|
||||||
submission, and rollout without requesting further confirmation.
|
before asking. If the build would be distributed automatically, name the exact
|
||||||
|
TestFlight groups or testers in the request and include that effect in the fresh
|
||||||
|
authorization. If the effect cannot be established, stop; changing an automatic-
|
||||||
|
distribution setting also requires separate authorization.
|
||||||
|
|
||||||
- **Android**: `bundle exec fastlane android release aab:<absolute path>`
|
Even when the original request includes building, releasing, uploading, or
|
||||||
commits the release on the production track (pass `track:` to target another
|
submitting, require this fresh confirmation once the exact verified artifact and
|
||||||
track, `rollout:0.1` for a staged rollout). Play review runs automatically;
|
destination are known. End the request with: `Upload this verified artifact to
|
||||||
rollout proceeds on approval. Use the `upload` lane only when the request is
|
<destination> now?` A clear affirmative response authorizes one upload of that
|
||||||
explicitly upload-only.
|
artifact to that destination, plus only any automatic TestFlight distribution
|
||||||
- **iOS**: `bundle exec fastlane ios release ipa:<absolute path>` uploads,
|
explicitly disclosed in the request. Run `eas submit` with the explicit
|
||||||
creates the App Store version, submits for App Review, and releases
|
`--platform`, `--profile production`, `--path`, and `--wait` arguments; never
|
||||||
automatically on approval (pass `phased_release:true` for a 7-day phased
|
select `--latest`. If the result is ambiguous, inspect EAS and the store before
|
||||||
release, `release_notes:"..."` for What's New). Use the `upload` lane for
|
doing anything else, do not retry, and ask again before another upload.
|
||||||
TestFlight-only requests. `eas submit` remains valid for upload-only flows;
|
|
||||||
when used, pass explicit `--platform`, `--profile production`, `--path`, and
|
|
||||||
`--wait` — never `--latest`.
|
|
||||||
|
|
||||||
App Store Connect API group association does not establish the automatic-
|
Apply this gate to every upload method, including EAS, Fastlane, Xcode Organizer,
|
||||||
distribution switch. Verify that switch in an authenticated App Store Connect UI
|
and the store consoles. A later public rollout, App Review submission, or
|
||||||
session before upload; if that UI verification is unavailable, stop rather
|
additional TestFlight distribution requires another fresh authorization.
|
||||||
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
|
||||||
@@ -118,30 +58,5 @@ 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
|
reject or rebuild only after reading the exported IPA itself. Report the artifact,
|
||||||
artifact, version/build number, upload and submission results, rollout state,
|
version/build number, upload result when authorized, and store read-back 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,11 +39,3 @@ 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,14 +24,9 @@ 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" ]; then
|
if [ "$BASE_BRANCH" = "main" ] && [ "$HEAD_BRANCH" != "staging" ]; then
|
||||||
case "$HEAD_BRANCH" in
|
echo "Only staging may be promoted to main" >&2
|
||||||
staging|hotfix/*|release/*) ;;
|
exit 1
|
||||||
*)
|
|
||||||
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,7 +76,6 @@ 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` |
|
||||||
|
|||||||
@@ -2,21 +2,6 @@
|
|||||||
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,6 +11,5 @@ 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,5 +13,3 @@ 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,8 +12,5 @@ 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('hermes-compiler/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/hermesc/%OS-BIN%/hermesc"
|
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/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](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
* Set this to true in release builds to optimize the app using R8.
|
||||||
*/
|
*/
|
||||||
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||||
|
|
||||||
@@ -92,9 +92,8 @@ android {
|
|||||||
applicationId 'com.sauci.app'
|
applicationId 'com.sauci.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 51
|
versionCode 49
|
||||||
versionName "1.0.8"
|
versionName "1.0.7"
|
||||||
|
|
||||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -182,4 +181,54 @@ 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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<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" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<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" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<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:enableOnBackInvokedCallback="false" android:requestLegacyExternalStorage="true" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
|
<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">
|
||||||
<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,14 +30,13 @@
|
|||||||
<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|smallestScreenSize|assetsPaths" 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" 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"/>
|
||||||
@@ -47,6 +46,7 @@
|
|||||||
<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="false" android:label="Live Draw">
|
<receiver android:name=".widget.LiveDraw" android:exported="true" 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,5 +1,4 @@
|
|||||||
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
|
||||||
@@ -16,10 +15,7 @@ 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,27 +5,40 @@ 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.ReactNativeApplicationEntryPoint.loadReactNative
|
import com.facebook.react.ReactNativeHost
|
||||||
import com.facebook.react.ReactPackage
|
import com.facebook.react.ReactPackage
|
||||||
import com.facebook.react.ReactHost
|
import com.facebook.react.ReactHost
|
||||||
import com.facebook.react.common.ReleaseLevel
|
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||||
|
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||||
|
import com.facebook.react.common.ReleaseLevel
|
||||||
|
|
||||||
import expo.modules.ApplicationLifecycleDispatcher
|
import expo.modules.ApplicationLifecycleDispatcher
|
||||||
import expo.modules.ExpoReactHostFactory
|
import expo.modules.ReactNativeHostWrapper
|
||||||
|
|
||||||
class MainApplication : Application(), ReactApplication {
|
class MainApplication : Application(), ReactApplication {
|
||||||
|
|
||||||
override val reactHost: ReactHost by lazy {
|
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||||
ExpoReactHostFactory.getDefaultReactHost(
|
this,
|
||||||
context = applicationContext,
|
object : DefaultReactNativeHost(this) {
|
||||||
packageList =
|
override fun getPackages(): List<ReactPackage> {
|
||||||
PackageList(this).packages.apply {
|
val packages = PackageList(this).packages
|
||||||
// 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:
|
||||||
// add(MyReactNativePackage())
|
// packages.add(new 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,6 +1,84 @@
|
|||||||
package com.sauci.app.widget;
|
package com.sauci.app.widget;
|
||||||
|
|
||||||
import com.reactnativeandroidwidget.RNWidgetProvider;
|
import android.appwidget.AppWidgetManager;
|
||||||
|
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;
|
||||||
|
|
||||||
public class LiveDraw extends RNWidgetProvider {
|
import com.sauci.app.R;
|
||||||
|
|
||||||
|
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: 21 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 10 KiB |
@@ -2,5 +2,6 @@
|
|||||||
<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_system_ui_user_interface_style" translatable="false">automatic</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="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||||
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</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>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.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">@android:color/transparent</item>
|
<item name="android:statusBarColor">#1a1a2e</item>
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
<style name="ResetEditText" parent="@android:style/Widget.EditText">
|
||||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
<item name="android:padding">0dp</item>
|
||||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
<item name="android:textColorHint">#c8c8c8</item>
|
||||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
<item name="android:textColor">@android:color/black</item>
|
||||||
<item name="android:windowSplashScreenBehavior">icon_preferred</item>
|
</style>
|
||||||
|
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||||
|
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -4,18 +4,10 @@
|
|||||||
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.4'
|
classpath 'com.google.gms:google-services:4.4.1'
|
||||||
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=-Xmx4096m -XX:MaxMetaspaceSize=1024m
|
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||||
|
|
||||||
# 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,11 +41,6 @@ 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)
|
||||||
@@ -60,7 +55,16 @@ 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
|
||||||
|
|
||||||
expo.inlineModules.watchedDirectories=[]
|
|
||||||
android.compileSdkVersion=36
|
|
||||||
android.targetSdkVersion=36
|
android.targetSdkVersion=36
|
||||||
android.buildToolsVersion=36.0.0
|
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.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-9.3.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-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 the original authors.
|
# Copyright © 2015-2021 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,7 +86,8 @@ 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\n' "$PWD" ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$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
|
||||||
@@ -114,6 +115,7 @@ 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.
|
||||||
@@ -171,6 +173,7 @@ 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" )
|
||||||
|
|
||||||
@@ -203,14 +206,15 @@ 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, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_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" \
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
@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
|
||||||
@@ -75,10 +70,11 @@ 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%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
: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.8',
|
runtimeVersion: '1.0.7',
|
||||||
updates: {
|
updates: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url: OTA_MANIFEST_URL,
|
url: OTA_MANIFEST_URL,
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Sauci",
|
"name": "Sauci",
|
||||||
"slug": "sauci",
|
"slug": "sauci",
|
||||||
"version": "1.0.8",
|
"version": "1.0.7",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"scheme": "app.sauci",
|
"scheme": "app.sauci",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"platforms": [
|
"newArchEnabled": true,
|
||||||
"ios",
|
"splash": {
|
||||||
"android"
|
"image": "./assets/splash-icon.png",
|
||||||
],
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#1a1a2e"
|
||||||
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": false,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.sauci.app",
|
"bundleIdentifier": "com.sauci.app",
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
"group.com.sauci.app"
|
"group.com.sauci.app"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"buildNumber": "49",
|
"buildNumber": "46",
|
||||||
"usesAppleSignIn": true,
|
"usesAppleSignIn": true,
|
||||||
"googleServicesFile": "./GoogleService-Info.plist",
|
"googleServicesFile": "./GoogleService-Info.plist",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
@@ -187,7 +189,7 @@
|
|||||||
"backgroundColor": "#1a1a2e"
|
"backgroundColor": "#1a1a2e"
|
||||||
},
|
},
|
||||||
"package": "com.sauci.app",
|
"package": "com.sauci.app",
|
||||||
"versionCode": 51,
|
"versionCode": 49,
|
||||||
"googleServicesFile": "./google-services.json",
|
"googleServicesFile": "./google-services.json",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"RECEIVE_BOOT_COMPLETED",
|
"RECEIVE_BOOT_COMPLETED",
|
||||||
@@ -221,14 +223,6 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -326,10 +320,7 @@
|
|||||||
"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,12 +69,10 @@ 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);
|
||||||
|
|
||||||
@@ -106,30 +104,21 @@ 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 && userId) {
|
if (matches.length > 0) {
|
||||||
const seen = await hasSeenMatchesTutorial(userId);
|
const seen = await hasSeenMatchesTutorial();
|
||||||
if (isActive && !tutorialDismissedRef.current && !seen) {
|
if (!seen) {
|
||||||
setShowTutorial(true);
|
setShowTutorial(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
void checkTutorial();
|
checkTutorial();
|
||||||
|
}, [matches.length])
|
||||||
return () => {
|
|
||||||
isActive = false;
|
|
||||||
};
|
|
||||||
}, [matches.length, userId])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTutorialComplete = async () => {
|
const handleTutorialComplete = async () => {
|
||||||
if (!userId) return;
|
await markMatchesTutorialSeen();
|
||||||
|
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.border,
|
borderBottomColor: colors.border,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ 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,
|
||||||
@@ -30,9 +32,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 { REQUIRED_ONBOARDING_VERSION, ONBOARDING_OFFERING_ID } from '../../src/constants/onboarding';
|
import { useAvatarPicker } from '../../src/hooks/useAvatarPicker';
|
||||||
import { activeOnboardingStage, initialOnboardingStage, previousOnboardingStage, type OnboardingStage } from '../../src/lib/onboardingStages';
|
import { REQUIRED_ONBOARDING_VERSION, ONBOARDING_OFFERING_ID } from '../../src/constants/onboarding';
|
||||||
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' },
|
||||||
@@ -49,17 +51,15 @@ 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 = OnboardingStage;
|
type Stage = 'avatar' | 'name' | 'gender' | 'purpose' | 'notifications';
|
||||||
|
|
||||||
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>(() => initialOnboardingStage(initialUser.current?.name));
|
const [stage, setStage] = useState<Stage>('avatar');
|
||||||
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,18 +67,38 @@ 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 pendingDisplayName = pendingAppleDisplayName?.userId === userId ? pendingAppleDisplayName?.name : undefined;
|
const {
|
||||||
const displayName = persistedUserName?.trim() || pendingDisplayName || name;
|
avatarUri,
|
||||||
const hasStoredDisplayName = Boolean(persistedUserName?.trim() || pendingDisplayName);
|
isUploading: isUploadingAvatar,
|
||||||
const activeStage = activeOnboardingStage(stage, persistedUserName, pendingDisplayName);
|
showPicker: handleAvatarPress,
|
||||||
const previousStage = previousOnboardingStage(activeStage, hasStoredDisplayName);
|
uploadAvatar,
|
||||||
|
} = 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');
|
||||||
@@ -134,7 +154,7 @@ export default function OnboardingScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
console.log('[Onboarding] handleComplete called, user:', userId, 'name:', displayName);
|
console.log('[Onboarding] handleComplete called, user:', userId, 'name:', name);
|
||||||
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.');
|
||||||
@@ -145,10 +165,19 @@ 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: displayName.trim(),
|
name: name.trim(),
|
||||||
gender,
|
gender,
|
||||||
usage_reason: usageReason,
|
usage_reason: usageReason,
|
||||||
// Preserve the safest legacy profile values. Catalogue
|
// Preserve the safest legacy profile values. Catalogue
|
||||||
@@ -162,6 +191,10 @@ 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...');
|
||||||
@@ -198,8 +231,72 @@ export default function OnboardingScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStage = () => {
|
const renderStage = () => {
|
||||||
switch (activeStage) {
|
switch (stage) {
|
||||||
case 'name':
|
case 'avatar':
|
||||||
|
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"
|
||||||
@@ -208,12 +305,16 @@ export default function OnboardingScreen() {
|
|||||||
style={styles.stageContainer}
|
style={styles.stageContainer}
|
||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<LinearGradient
|
{avatarUri ? (
|
||||||
colors={gradients.primary as [string, string]}
|
<Image source={{ uri: avatarUri }} style={styles.headerAvatar} />
|
||||||
style={styles.iconContainer}
|
) : (
|
||||||
>
|
<LinearGradient
|
||||||
<Ionicons name="person" size={40} color={colors.text} />
|
colors={gradients.primary as [string, string]}
|
||||||
</LinearGradient>
|
style={styles.iconContainer}
|
||||||
|
>
|
||||||
|
<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!
|
||||||
@@ -267,7 +368,7 @@ export default function OnboardingScreen() {
|
|||||||
style={styles.stageContainer}
|
style={styles.stageContainer}
|
||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.greeting}>Hey, {displayName.trim()}!</Text>
|
<Text style={styles.greeting}>Hey, {name.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>
|
||||||
@@ -475,24 +576,28 @@ 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}>
|
||||||
{previousStage ? (
|
{stage !== 'avatar' ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
setStage(previousStage);
|
if (stage === 'notifications') setStage('purpose');
|
||||||
}}
|
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, activeStage === 'name' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'avatar' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, activeStage === 'gender' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'name' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, activeStage === 'purpose' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'gender' && styles.progressDotActive]} />
|
||||||
<View style={[styles.progressDot, activeStage === 'notifications' && styles.progressDotActive]} />
|
<View style={[styles.progressDot, stage === 'purpose' && styles.progressDotActive]} />
|
||||||
|
<View style={[styles.progressDot, stage === 'notifications' && styles.progressDotActive]} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.backButtonPlaceholder} />
|
<View style={styles.backButtonPlaceholder} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -548,6 +653,18 @@ 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',
|
||||||
@@ -640,6 +757,47 @@ 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,11 +46,6 @@ 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";
|
||||||
@@ -295,7 +290,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 { data, error } = await authClient.auth.signInWithIdToken({
|
const { error } = await authClient.auth.signInWithIdToken({
|
||||||
provider: "apple",
|
provider: "apple",
|
||||||
token: credential.identityToken,
|
token: credential.identityToken,
|
||||||
});
|
});
|
||||||
@@ -304,36 +299,6 @@ 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,11 +5,6 @@ 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";
|
||||||
|
|
||||||
@@ -78,7 +73,6 @@ export default function RootLayout() {
|
|||||||
analyticsInitialized.current = true;
|
analyticsInitialized.current = true;
|
||||||
initAnalytics();
|
initAnalytics();
|
||||||
Events.appOpened("cold");
|
Events.appOpened("cold");
|
||||||
logAppStarted();
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -121,15 +115,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();
|
||||||
@@ -206,15 +200,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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.border,
|
borderBottomColor: colors.border,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
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,86 +45,4 @@ 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,6 +28,3 @@ project.xcworkspace
|
|||||||
|
|
||||||
# CocoaPods
|
# CocoaPods
|
||||||
/Pods/
|
/Pods/
|
||||||
|
|
||||||
# @bacons/apple-targets generated entitlements cache
|
|
||||||
/.targets/
|
|
||||||
@@ -1,28 +1,15 @@
|
|||||||
# 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 {}
|
||||||
|
|
||||||
def ccache_enabled?(podfile_properties)
|
ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
|
||||||
# Environment variable takes precedence
|
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||||
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
|
|
||||||
|
|
||||||
# Fall back to Podfile properties
|
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||||
podfile_properties['apple.ccacheEnabled'] == 'true'
|
install! 'cocoapods',
|
||||||
end
|
:deterministic_uuids => false
|
||||||
|
|
||||||
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!
|
||||||
|
|
||||||
@@ -36,8 +23,7 @@ target 'Sauci' do
|
|||||||
'node',
|
'node',
|
||||||
'--no-warnings',
|
'--no-warnings',
|
||||||
'--eval',
|
'--eval',
|
||||||
'require(\'expo/bin/autolinking\')',
|
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
|
||||||
'expo-modules-autolinking',
|
|
||||||
'react-native-config',
|
'react-native-config',
|
||||||
'--json',
|
'--json',
|
||||||
'--platform',
|
'--platform',
|
||||||
@@ -63,8 +49,34 @@ target 'Sauci' do
|
|||||||
installer,
|
installer,
|
||||||
config[:reactNativePath],
|
config[:reactNativePath],
|
||||||
:mac_catalyst_enabled => false,
|
:mac_catalyst_enabled => false,
|
||||||
:ccache_enabled => ccache_enabled?(podfile_properties),
|
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"expo.jsEngine": "hermes",
|
"expo.jsEngine": "hermes",
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
"expo.inlineModules.watchedDirectories": "[]",
|
"newArchEnabled": "true",
|
||||||
"expo.inlineModules.xcodeProjectTargets": "{\"mainTarget\":\"Sauci\",\"targets\":[]}",
|
"ios.useFrameworks": "static",
|
||||||
"ios.forceStaticLinking": "[]",
|
"apple.extraPods": "[]",
|
||||||
"apple.privacyManifestAggregationEnabled": "true",
|
"apple.ccacheEnabled": "false",
|
||||||
"EXPO_USE_PRECOMPILED_MODULES": "true",
|
"apple.privacyManifestAggregationEnabled": "true"
|
||||||
"ios.useFrameworks": "static"
|
|
||||||
}
|
}
|
||||||
@@ -3,18 +3,19 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 46;
|
||||||
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 */; };
|
||||||
3F202C2A859975F48A83ADD7 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779267BD380A5D29590F1BDC /* ExpoModulesProvider.swift */; };
|
5254BDADE70741A0B2504FB1 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = F744EAD4ABA54509966475D6 /* noop-file.swift */; };
|
||||||
45941E25D336B6152BFCEFB9 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B482C7D915C22B3F33CA9841 /* Pods_Sauci.framework */; };
|
7B71EEB2AF682FA7AFF556D5 /* Pods_Sauci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDDAD8C12C38D3BD0A76D345 /* Pods_Sauci.framework */; };
|
||||||
804CD03F6B584D08B7803C63 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 69E91809C21A4977A3249D53 /* PrivacyInfo.xcprivacy */; };
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||||
D60EEEF252214D45B67A0063 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D58CE235EF5F40F58FB8111D /* GoogleService-Info.plist */; };
|
CEF2F278ABBC5FBFD470A0F7 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7B5C17768E3B74AC152531F /* PrivacyInfo.xcprivacy */; };
|
||||||
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 */; };
|
||||||
@@ -23,11 +24,11 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
XX77D941E1DB38490B8F4EXX /* PBXContainerItemProxy */ = {
|
XXA7C7D5580082A497FD95XX /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||||
proxyType = 1;
|
proxyType = 1;
|
||||||
remoteGlobalIDString = XXB3405CC208C2ECA4797FXX;
|
remoteGlobalIDString = XX540FB4ABC75CE91A6036XX;
|
||||||
remoteInfo = LiveDrawWidget;
|
remoteInfo = LiveDrawWidget;
|
||||||
};
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
@@ -50,17 +51,18 @@
|
|||||||
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>"; };
|
||||||
69E91809C21A4977A3249D53 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; 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>"; };
|
||||||
779267BD380A5D29590F1BDC /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; 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>"; };
|
||||||
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>"; };
|
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>"; };
|
||||||
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>"; };
|
||||||
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>"; };
|
D7B5C17768E3B74AC152531F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = undefined; name = PrivacyInfo.xcprivacy; path = Sauci/PrivacyInfo.xcprivacy; 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>"; };
|
||||||
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>"; };
|
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>"; };
|
||||||
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>"; };
|
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Sauci/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||||
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
XX470101BAEB8E0BBC8B0DXX /* WidgetKit.framework */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = undefined; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
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; };
|
||||||
@@ -69,21 +71,21 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
XX6FB9E6678087E3AAC658XX /* Exceptions for "widget" folder in "LiveDrawWidget" target */ = {
|
XX020388E3BB9C764E774FXX /* 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 = XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */;
|
target = XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */;
|
||||||
};
|
};
|
||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
XXBB44BF07EB68666150ABXX /* widget */ = {
|
XX683E29AD7AD8279E17B2XX /* widget */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
XX6FB9E6678087E3AAC658XX /* Exceptions for "widget" folder in "LiveDrawWidget" target */,
|
XX020388E3BB9C764E774FXX /* Exceptions for "widget" folder in "LiveDrawWidget" target */,
|
||||||
);
|
);
|
||||||
explicitFileTypes = {
|
explicitFileTypes = {
|
||||||
};
|
};
|
||||||
@@ -99,7 +101,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
45941E25D336B6152BFCEFB9 /* Pods_Sauci.framework in Frameworks */,
|
7B71EEB2AF682FA7AFF556D5 /* Pods_Sauci.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -120,32 +122,15 @@
|
|||||||
13B07FAE1A68108700A75B9A /* Sauci */ = {
|
13B07FAE1A68108700A75B9A /* Sauci */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
|
||||||
F11748442D0722820044C1D9 /* Sauci-Bridging-Header.h */,
|
|
||||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||||
|
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||||
69E91809C21A4977A3249D53 /* PrivacyInfo.xcprivacy */,
|
A9280FDF3E0D43AC90382DBC /* GoogleService-Info.plist */,
|
||||||
D58CE235EF5F40F58FB8111D /* GoogleService-Info.plist */,
|
F744EAD4ABA54509966475D6 /* noop-file.swift */,
|
||||||
);
|
DA536CAC88B349A59DFC1F1E /* Sauci-Bridging-Header.h */,
|
||||||
name = Sauci;
|
D7B5C17768E3B74AC152531F /* PrivacyInfo.xcprivacy */,
|
||||||
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>";
|
||||||
@@ -156,9 +141,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>";
|
||||||
@@ -178,8 +163,8 @@
|
|||||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||||
83CBBA001A601CBA00E9B192 /* Products */,
|
83CBBA001A601CBA00E9B192 /* Products */,
|
||||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||||
1957149DB83AAF8603C60859 /* Pods */,
|
D65327D7A22EEC0BE12398D9 /* Pods */,
|
||||||
CC8FEB5C5178B9E5E71D4F61 /* ExpoModulesProviders */,
|
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */,
|
||||||
);
|
);
|
||||||
indentWidth = 2;
|
indentWidth = 2;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -195,6 +180,14 @@
|
|||||||
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 = (
|
||||||
@@ -204,10 +197,19 @@
|
|||||||
path = Sauci/Supporting;
|
path = Sauci/Supporting;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CC8FEB5C5178B9E5E71D4F61 /* ExpoModulesProviders */ = {
|
D65327D7A22EEC0BE12398D9 /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2BFCD64E7EEB594CC2D9E261 /* Sauci */,
|
6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */,
|
||||||
|
7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
92DBD88DE9BF7D494EA9DA96 /* Sauci */,
|
||||||
);
|
);
|
||||||
name = ExpoModulesProviders;
|
name = ExpoModulesProviders;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -215,7 +217,7 @@
|
|||||||
XX3E41C1850246162C0061XX /* expo:targets */ = {
|
XX3E41C1850246162C0061XX /* expo:targets */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
XXBB44BF07EB68666150ABXX /* widget */,
|
XX683E29AD7AD8279E17B2XX /* widget */,
|
||||||
);
|
);
|
||||||
name = "expo:targets";
|
name = "expo:targets";
|
||||||
path = ../targets;
|
path = ../targets;
|
||||||
@@ -229,31 +231,30 @@
|
|||||||
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 */,
|
||||||
E6F7433229E3C1448FC9F062 /* [Expo] Configure project */,
|
DC3D179C6B62F16DA5F17C2F /* [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 */,
|
||||||
503A0FD3DBA862B4BE649BBB /* [CP] Embed Pods Frameworks */,
|
A1248897D24903117793599B /* [CP] Embed Pods Frameworks */,
|
||||||
8490E10253F91A7BEB80F555 /* [CP-User] [RNFB] Core Configuration */,
|
FB4B019D835055733ACDE109 /* [CP-User] [RNFB] Core Configuration */,
|
||||||
0EE1B67C9AAD8BD8562E800E /* [CP-User] [RNFB] Crashlytics Configuration */,
|
563ACE2651AC39438F3B399B /* [CP-User] [RNFB] Crashlytics Configuration */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
XX5D3A21868E45E6E5BDC3XX /* PBXTargetDependency */,
|
XX97289CEC7994AD5F4142XX /* 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";
|
||||||
};
|
};
|
||||||
XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */ = {
|
XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = XX35D62F4FBF2C9086E8C9XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */;
|
buildConfigurationList = XX63FAB1BD532DA0D8AE18XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
XX074B8811C1CF8E8A3D74XX /* Frameworks */,
|
XX074B8811C1CF8E8A3D74XX /* Frameworks */,
|
||||||
XXE45443DC38DCFE1DB622XX /* Sources */,
|
XXE45443DC38DCFE1DB622XX /* Sources */,
|
||||||
@@ -264,7 +265,7 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
XXBB44BF07EB68666150ABXX /* widget */,
|
XX683E29AD7AD8279E17B2XX /* widget */,
|
||||||
);
|
);
|
||||||
name = LiveDrawWidget;
|
name = LiveDrawWidget;
|
||||||
productName = LiveDrawWidget;
|
productName = LiveDrawWidget;
|
||||||
@@ -282,7 +283,7 @@
|
|||||||
13B07F861A680F5B00A75B9A = {
|
13B07F861A680F5B00A75B9A = {
|
||||||
LastSwiftMigration = 1250;
|
LastSwiftMigration = 1250;
|
||||||
};
|
};
|
||||||
XXB3405CC208C2ECA4797FXX = {
|
XX540FB4ABC75CE91A6036XX = {
|
||||||
CreatedOnToolsVersion = 14.3;
|
CreatedOnToolsVersion = 14.3;
|
||||||
DevelopmentTeam = BXVPBATDZY;
|
DevelopmentTeam = BXVPBATDZY;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
@@ -303,7 +304,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
13B07F861A680F5B00A75B9A /* Sauci */,
|
13B07F861A680F5B00A75B9A /* Sauci */,
|
||||||
XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */,
|
XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -316,8 +317,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 */,
|
||||||
804CD03F6B584D08B7803C63 /* PrivacyInfo.xcprivacy in Resources */,
|
FDFABE1A1A3547B1B22DD2DD /* GoogleService-Info.plist in Resources */,
|
||||||
D60EEEF252214D45B67A0063 /* GoogleService-Info.plist in Resources */,
|
CEF2F278ABBC5FBFD470A0F7 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -340,8 +341,6 @@
|
|||||||
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 = (
|
||||||
@@ -374,7 +373,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;
|
||||||
};
|
};
|
||||||
0EE1B67C9AAD8BD8562E800E /* [CP-User] [RNFB] Crashlytics Configuration */ = {
|
563ACE2651AC39438F3B399B /* [CP-User] [RNFB] Crashlytics Configuration */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -388,52 +387,6 @@
|
|||||||
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;
|
||||||
@@ -445,10 +398,9 @@
|
|||||||
"${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}/EXUpdates/EXUpdates.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.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",
|
||||||
@@ -462,16 +414,18 @@
|
|||||||
"${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 = (
|
||||||
@@ -479,10 +433,9 @@
|
|||||||
"${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}/EXUpdates.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.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",
|
||||||
@@ -496,54 +449,43 @@
|
|||||||
"${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;
|
||||||
};
|
};
|
||||||
8490E10253F91A7BEB80F555 /* [CP-User] [RNFB] Core Configuration */ = {
|
A1248897D24903117793599B /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
|
"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh",
|
||||||
);
|
"${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 = "# 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";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Sauci/Pods-Sauci-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
E6F7433229E3C1448FC9F062 /* [Expo] Configure project */ = {
|
DC3D179C6B62F16DA5F17C2F /* [Expo] Configure project */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -567,6 +509,19 @@
|
|||||||
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 */
|
||||||
@@ -575,7 +530,8 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
|
||||||
3F202C2A859975F48A83ADD7 /* ExpoModulesProvider.swift in Sources */,
|
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||||
|
5254BDADE70741A0B2504FB1 /* noop-file.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -589,17 +545,17 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
XX5D3A21868E45E6E5BDC3XX /* PBXTargetDependency */ = {
|
XX97289CEC7994AD5F4142XX /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = XXB3405CC208C2ECA4797FXX /* LiveDrawWidget */;
|
target = XX540FB4ABC75CE91A6036XX /* LiveDrawWidget */;
|
||||||
targetProxy = XX77D941E1DB38490B8F4EXX /* PBXContainerItemProxy */;
|
targetProxy = XXA7C7D5580082A497FD95XX /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = FEACE2E5B4EA50AFBF4DDF34 /* Pods-Sauci.debug.xcconfig */;
|
baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-Sauci.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
@@ -612,13 +568,9 @@
|
|||||||
"FB_SONARKIT_ENABLED=1",
|
"FB_SONARKIT_ENABLED=1",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Sauci/Info.plist;
|
INFOPLIST_FILE = Sauci/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
"$(inherited)",
|
MARKETING_VERSION = 1.0.7;
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.4;
|
|
||||||
MARKETING_VERSION = 1.0.8;
|
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -637,7 +589,7 @@
|
|||||||
};
|
};
|
||||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = A172EA788EF476A7083810FE /* Pods-Sauci.release.xcconfig */;
|
baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-Sauci.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
@@ -645,13 +597,9 @@
|
|||||||
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 = 16.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
"$(inherited)",
|
MARKETING_VERSION = 1.0.7;
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.4;
|
|
||||||
MARKETING_VERSION = 1.0.8;
|
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -716,39 +664,29 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"${PODS_ROOT}/ReactCommon",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
||||||
"${PODS_ROOT}/ReactCommon/react/nativemodule/core",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
||||||
"${PODS_ROOT}/React-runtimeexecutor",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
||||||
"${PODS_ROOT}/React-runtimeexecutor/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
||||||
"${PODS_ROOT}/ReactCommon-Samples",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
||||||
"${PODS_ROOT}/ReactCommon-Samples/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||||
"${PODS_ROOT}/React-Fabric/react/renderer/components/view/platform/cxx",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||||
"${PODS_ROOT}/React-NativeModulesApple",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||||
"${PODS_ROOT}/React-graphics",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers",
|
||||||
"${PODS_ROOT}/React-graphics/react/renderer/graphics/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/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_CFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(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;
|
||||||
@@ -795,43 +733,33 @@
|
|||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"${PODS_ROOT}/ReactCommon",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
||||||
"${PODS_ROOT}/ReactCommon/react/nativemodule/core",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
||||||
"${PODS_ROOT}/React-runtimeexecutor",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
||||||
"${PODS_ROOT}/React-runtimeexecutor/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
||||||
"${PODS_ROOT}/ReactCommon-Samples",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
||||||
"${PODS_ROOT}/ReactCommon-Samples/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||||
"${PODS_ROOT}/React-Fabric/react/renderer/components/view/platform/cxx",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||||
"${PODS_ROOT}/React-NativeModulesApple",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||||
"${PODS_ROOT}/React-graphics",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers",
|
||||||
"${PODS_ROOT}/React-graphics/react/renderer/graphics/platform/ios",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/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_CFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(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;
|
||||||
};
|
};
|
||||||
XX0A0FB078256184BF7EA2XX /* Release */ = {
|
XX1230635DC14DD95E28A2XX /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
@@ -844,7 +772,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 49;
|
CURRENT_PROJECT_VERSION = 46;
|
||||||
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;
|
||||||
@@ -853,17 +781,12 @@
|
|||||||
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 = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
"$(inherited)",
|
MARKETING_VERSION = 1.0.7;
|
||||||
"@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;
|
||||||
@@ -873,7 +796,7 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
XX42CD38636C47E26CBE73XX /* Debug */ = {
|
XX710DB962925BCE8D59ACXX /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
@@ -885,7 +808,7 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 49;
|
CURRENT_PROJECT_VERSION = 46;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@@ -894,18 +817,13 @@
|
|||||||
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 = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
"$(inherited)",
|
MARKETING_VERSION = 1.0.7;
|
||||||
"@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;
|
||||||
@@ -936,11 +854,11 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
XX35D62F4FBF2C9086E8C9XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */ = {
|
XX63FAB1BD532DA0D8AE18XX /* Build configuration list for PBXNativeTarget "LiveDrawWidget" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
XX42CD38636C47E26CBE73XX /* Debug */,
|
XX710DB962925BCE8D59ACXX /* Debug */,
|
||||||
XX0A0FB078256184BF7EA2XX /* Release */,
|
XX1230635DC14DD95E28A2XX /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
internal import Expo
|
import Expo
|
||||||
import FirebaseCore
|
import FirebaseCore
|
||||||
import React
|
import React
|
||||||
import ReactAppDependencyProvider
|
import ReactAppDependencyProvider
|
||||||
|
|
||||||
@main
|
@UIApplicationMain
|
||||||
class AppDelegate: ExpoAppDelegate {
|
public class AppDelegate: ExpoAppDelegate {
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
|
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
|
||||||
@@ -20,12 +20,11 @@ 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)
|
||||||
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a
|
FirebaseApp.configure()
|
||||||
FirebaseApp.configure()
|
|
||||||
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
|
|
||||||
factory.startReactNative(
|
factory.startReactNative(
|
||||||
withModuleName: "main",
|
withModuleName: "main",
|
||||||
in: window,
|
in: window,
|
||||||
@@ -35,39 +34,42 @@ FirebaseApp.configure()
|
|||||||
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 {
|
||||||
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
|
super.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(application, continue: userActivity, restorationHandler: restorationHandler)
|
let result = RCTLinkingManager.application(
|
||||||
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
|
application,
|
||||||
|
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
|
||||||
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
RCTBundleURLProvider.sharedSettings()
|
||||||
|
.jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
||||||
#else
|
#else
|
||||||
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 135 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.8</string>
|
<string>1.0.7</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>49</string>
|
<string>46</string>
|
||||||
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
@@ -103,6 +103,13 @@
|
|||||||
<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,14 +24,6 @@
|
|||||||
<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>
|
||||||
@@ -41,171 +33,18 @@
|
|||||||
<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,12 +1,11 @@
|
|||||||
<?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="24093.7" 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="32700.99.1234" 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="24053.1"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
|
||||||
<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>
|
||||||
@@ -18,13 +17,15 @@
|
|||||||
<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="146.5" y="376" width="100" height="100"/>
|
<rect key="frame" x="0" y="0" width="414" height="736"/>
|
||||||
</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="centerX" secondItem="EXPO-ContainerView" secondAttribute="centerX" id="cad2ab56f97c5429bf29decf850647a4216861d4"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
|
||||||
<constraint firstItem="EXPO-SplashScreen" firstAttribute="centerY" secondItem="EXPO-ContainerView" secondAttribute="centerY" id="1a145271b085b6ce89b1405a310f5b1bb7656595"/>
|
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
|
||||||
|
<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>
|
||||||
@@ -35,10 +36,7 @@
|
|||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="SplashScreenLogo" width="100" height="100"/>
|
<image name="SplashScreenLogo" width="414" height="736"/>
|
||||||
<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,8 +31,6 @@ 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>
|
||||||
@@ -47,7 +45,7 @@ Ug==
|
|||||||
<string/>
|
<string/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>EXUpdatesRuntimeVersion</key>
|
<key>EXUpdatesRuntimeVersion</key>
|
||||||
<string>1.0.8</string>
|
<string>1.0.7</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>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
//
|
||||||
|
// @generated
|
||||||
|
// A blank Swift file must be created for native modules with Swift files to work correctly.
|
||||||
|
//
|
||||||
@@ -12,12 +12,6 @@ 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,11 +19,6 @@
|
|||||||
"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",
|
||||||
@@ -34,7 +29,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": "^5.0.0",
|
"@bacons/apple-targets": "^0.1.6",
|
||||||
"@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",
|
||||||
@@ -44,55 +39,54 @@
|
|||||||
"@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",
|
||||||
"@shopify/react-native-skia": "^2.6.9",
|
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||||
|
"@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": "~56.0.0",
|
"babel-preset-expo": "~54.0.12",
|
||||||
"base64-arraybuffer": "^1.0.2",
|
"base64-arraybuffer": "^1.0.2",
|
||||||
"expo": "^56.0.0",
|
"expo": "~54.0.0",
|
||||||
"expo-apple-authentication": "~56.0.4",
|
"expo-apple-authentication": "~8.0.8",
|
||||||
"expo-asset": "~56.0.24",
|
"expo-asset": "~12.0.13",
|
||||||
"expo-audio": "~56.0.13",
|
"expo-av": "~16.0.8",
|
||||||
"expo-blur": "~56.0.4",
|
"expo-blur": "~15.0.8",
|
||||||
"expo-build-properties": "~56.0.27",
|
"expo-build-properties": "~1.0.10",
|
||||||
"expo-clipboard": "~56.0.4",
|
"expo-clipboard": "~8.0.8",
|
||||||
"expo-constants": "~56.0.25",
|
"expo-constants": "~18.0.14",
|
||||||
"expo-dev-client": "~56.0.26",
|
"expo-dev-client": "~6.0.21",
|
||||||
"expo-file-system": "~56.0.11",
|
"expo-file-system": "~19.0.24",
|
||||||
"expo-font": "~56.0.7",
|
"expo-font": "~14.0.12",
|
||||||
"expo-haptics": "~56.0.3",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~56.0.12",
|
"expo-image": "~3.0.11",
|
||||||
"expo-image-picker": "~56.0.25",
|
"expo-image-picker": "~17.0.11",
|
||||||
"expo-linear-gradient": "~56.0.4",
|
"expo-linear-gradient": "~15.0.8",
|
||||||
"expo-linking": "~56.0.17",
|
"expo-linking": "~8.0.12",
|
||||||
"expo-local-authentication": "~56.0.5",
|
"expo-local-authentication": "~17.0.9",
|
||||||
"expo-media-library": "~56.0.11",
|
"expo-media-library": "~18.2.1",
|
||||||
"expo-notifications": "~56.0.25",
|
"expo-notifications": "~0.32.17",
|
||||||
"expo-observe": "~56.0.29",
|
"expo-router": "~6.0.24",
|
||||||
"expo-router": "~56.2.20",
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-secure-store": "~56.0.4",
|
"expo-sharing": "~14.0.8",
|
||||||
"expo-sharing": "~56.0.26",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-splash-screen": "~56.0.15",
|
"expo-updates": "~29.0.20",
|
||||||
"expo-status-bar": "~56.0.4",
|
"react": "19.1.0",
|
||||||
"expo-system-ui": "~56.0.5",
|
"react-dom": "19.1.0",
|
||||||
"expo-updates": "~56.0.26",
|
"react-native": "0.81.5",
|
||||||
"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.31.1",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-purchases": "^9.6.12",
|
"react-native-purchases": "^9.6.12",
|
||||||
"react-native-reanimated": "4.3.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.26.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-share": "^12.3.1",
|
"react-native-share": "^12.3.1",
|
||||||
"react-native-svg": "15.15.4",
|
"react-native-svg": "15.12.1",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-view-shot": "5.1.0",
|
"react-native-view-shot": "^4.0.3",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.8.3",
|
"react-native-worklets": "0.5.1",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -100,16 +94,15 @@
|
|||||||
"@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/node": "^20.19.0",
|
"@types/react": "~19.1.10",
|
||||||
"@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": "~56.0.4",
|
"eslint-config-expo": "~10.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expo": "~56.0.5",
|
"jest-expo": "~54.0.18",
|
||||||
"react-test-renderer": "19.2.3",
|
"react-test-renderer": "19.1.0",
|
||||||
"typescript": "~6.0.3"
|
"typescript": "~5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,8 +141,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare real filesystem paths. A URL pathname percent-encodes spaces, so
|
// Compare real filesystem paths: a URL pathname percent-encodes spaces, so
|
||||||
// comparing it to process.argv[1] silently skips main() under any checkout
|
// 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
|
// whose path contains a space.
|
||||||
// having done nothing.
|
|
||||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#!/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();
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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,43 +1,31 @@
|
|||||||
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 { AudioModule, requestRecordingPermissionsAsync, setAudioModeAsync } from 'expo-audio';
|
import { Audio } from 'expo-av';
|
||||||
|
|
||||||
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> = {}) => ({
|
||||||
getStatus: jest.fn(() => ({ isRecording: true, durationMillis: 2400, canRecord: true, url: null })),
|
getStatusAsync: jest.fn(async () => ({ isRecording: true, durationMillis: 2400 })),
|
||||||
prepareToRecordAsync: jest.fn(async () => undefined),
|
stopAndUnloadAsync: jest.fn(async () => undefined),
|
||||||
record: jest.fn(),
|
getURI: jest.fn(() => 'file://recording.m4a'),
|
||||||
stop: jest.fn(async () => undefined),
|
setOnRecordingStatusUpdate: jest.fn(),
|
||||||
uri: 'file://recording.m4a',
|
setProgressUpdateInterval: jest.fn(),
|
||||||
...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);
|
||||||
(requestRecordingPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted' });
|
(Audio.requestPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted' });
|
||||||
(setAudioModeAsync as jest.Mock).mockResolvedValue(undefined);
|
(Audio.setAudioModeAsync as jest.Mock).mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => jest.restoreAllMocks());
|
||||||
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 () => {
|
||||||
(requestRecordingPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied' });
|
(Audio.requestPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied' });
|
||||||
const { result } = renderHook(() => useAudioRecorder());
|
const { result } = renderHook(() => useAudioRecorder());
|
||||||
|
|
||||||
let started = true;
|
let started = true;
|
||||||
@@ -48,22 +36,21 @@ describe('useAudioRecorder', () => {
|
|||||||
'Microphone Permission',
|
'Microphone Permission',
|
||||||
'Please allow microphone access to record audio.'
|
'Please allow microphone access to record audio.'
|
||||||
);
|
);
|
||||||
expect(AudioRecorderMock).not.toHaveBeenCalled();
|
expect(Audio.Recording.createAsync).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();
|
||||||
AudioRecorderMock.mockImplementation(() => recording);
|
(Audio.Recording.createAsync as jest.Mock).mockResolvedValue({ 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.prepareToRecordAsync).toHaveBeenCalled();
|
expect(recording.setProgressUpdateInterval).toHaveBeenCalledWith(100);
|
||||||
expect(recording.record).toHaveBeenCalled();
|
|
||||||
|
|
||||||
// Duration is reported by polling the recorder status, not by a push event.
|
const statusCallback = recording.setOnRecordingStatusUpdate.mock.calls[0][0];
|
||||||
await act(async () => { jest.advanceTimersByTime(100); });
|
act(() => statusCallback({ isRecording: true, durationMillis: 2400 }));
|
||||||
expect(result.current.durationSeconds).toBe(2);
|
expect(result.current.durationSeconds).toBe(2);
|
||||||
|
|
||||||
let completed: unknown;
|
let completed: unknown;
|
||||||
@@ -79,55 +66,50 @@ 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({
|
||||||
getStatus: jest.fn(() => ({ isRecording: true, durationMillis: 1000, canRecord: true, url: null })),
|
getStatusAsync: jest.fn(async () => ({ isRecording: true, durationMillis: 1000 })),
|
||||||
});
|
});
|
||||||
AudioRecorderMock.mockImplementation(() => recording);
|
(Audio.Recording.createAsync as jest.Mock).mockResolvedValue({ 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(); });
|
||||||
await act(async () => { jest.advanceTimersByTime(100); });
|
const statusCallback = recording.setOnRecordingStatusUpdate.mock.calls[0][0];
|
||||||
// Flush the microtasks queued by the auto-stop call.
|
await act(async () => { statusCallback({ isRecording: true, durationMillis: 1000 }); });
|
||||||
await act(async () => { await Promise.resolve(); });
|
|
||||||
|
|
||||||
expect(recording.stop).toHaveBeenCalled();
|
expect(recording.stopAndUnloadAsync).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();
|
||||||
AudioRecorderMock
|
(Audio.Recording.createAsync as jest.Mock)
|
||||||
.mockImplementationOnce(() => first)
|
.mockResolvedValueOnce({ recording: first })
|
||||||
.mockImplementationOnce(() => second);
|
.mockResolvedValueOnce({ recording: 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.stop).toHaveBeenCalled();
|
expect(first.stopAndUnloadAsync).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.stop).toHaveBeenCalled();
|
expect(second.stopAndUnloadAsync).toHaveBeenCalled();
|
||||||
expect(setAudioModeAsync).toHaveBeenLastCalledWith({
|
expect(Audio.setAudioModeAsync).toHaveBeenLastCalledWith({
|
||||||
allowsRecording: false,
|
allowsRecordingIOS: false,
|
||||||
playsInSilentMode: true,
|
playsInSilentModeIOS: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recovers when starting or stopping throws', async () => {
|
it('recovers when starting or stopping throws', async () => {
|
||||||
AudioRecorderMock.mockImplementationOnce(() => {
|
(Audio.Recording.createAsync as jest.Mock).mockRejectedValueOnce(new Error('native unavailable'));
|
||||||
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({
|
const recording = makeRecording({ getStatusAsync: jest.fn(async () => { throw new Error('lost'); }) });
|
||||||
getStatus: jest.fn(() => { throw new Error('lost'); }),
|
(Audio.Recording.createAsync as jest.Mock).mockResolvedValueOnce({ recording });
|
||||||
});
|
|
||||||
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,19 +1,14 @@
|
|||||||
import { act, renderHook } from '@testing-library/react-native';
|
import { act, renderHook } from '@testing-library/react-native';
|
||||||
import { ActionSheetIOS, Alert, Linking, Platform } from 'react-native';
|
import { ActionSheetIOS, Alert, 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(),
|
||||||
@@ -28,10 +23,7 @@ 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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,41 +64,4 @@ 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, apiRequestWithAccessToken } from "@/lib/apiClient";
|
import { ApiError, apiRequest } 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,17 +40,6 @@ 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" },
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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, apiRequestWithAccessToken } from '../../lib/apiClient';
|
import { apiClient } from '../../lib/apiClient';
|
||||||
import { profileSettingsApi } from '../../lib/profileSettingsApi';
|
import { profileSettingsApi } from '../../lib/profileSettingsApi';
|
||||||
|
|
||||||
jest.mock('../../lib/apiClient', () => ({
|
jest.mock('../../lib/apiClient', () => ({
|
||||||
@@ -7,7 +7,6 @@ jest.mock('../../lib/apiClient', () => ({
|
|||||||
post: jest.fn(),
|
post: jest.fn(),
|
||||||
patch: jest.fn(),
|
patch: jest.fn(),
|
||||||
},
|
},
|
||||||
apiRequestWithAccessToken: jest.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('profileSettingsApi', () => {
|
describe('profileSettingsApi', () => {
|
||||||
@@ -28,14 +27,4 @@ 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,14 +30,10 @@ 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,
|
||||||
@@ -46,7 +42,6 @@ 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);
|
||||||
@@ -69,13 +64,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 }, access_token: "token-me" } },
|
data: { session: { user: { id: "me", is_anonymous: true } } },
|
||||||
});
|
});
|
||||||
const getSpy = jest.spyOn(apiClient, "getWithAccessToken").mockResolvedValueOnce({ profile });
|
const getSpy = jest.spyOn(apiClient, "get").mockResolvedValueOnce({ profile });
|
||||||
|
|
||||||
await useAuthStore.getState().fetchUser();
|
await useAuthStore.getState().fetchUser();
|
||||||
|
|
||||||
expect(getSpy).toHaveBeenCalledWith("/v1/me", "token-me");
|
expect(getSpy).toHaveBeenCalledWith("/v1/me");
|
||||||
expect(useAuthStore.getState()).toMatchObject({
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
user: profile,
|
user: profile,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
@@ -119,9 +114,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" }, access_token: "token-me" } },
|
data: { session: { user: { id: "me" } } },
|
||||||
});
|
});
|
||||||
jest.spyOn(apiClient, "getWithAccessToken").mockRejectedValueOnce(new ApiError("Unauthorized", 401));
|
jest.spyOn(apiClient, "get").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();
|
||||||
@@ -140,123 +135,6 @@ 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);
|
||||||
|
|
||||||
@@ -280,14 +158,4 @@ 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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
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.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
},
|
},
|
||||||
keyboardView: {
|
keyboardView: {
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ const styles = StyleSheet.create({
|
|||||||
borderColor: colors.premium.goldLight,
|
borderColor: colors.premium.goldLight,
|
||||||
},
|
},
|
||||||
annualBackground: {
|
annualBackground: {
|
||||||
...StyleSheet.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: colors.premium.goldLight,
|
backgroundColor: colors.premium.goldLight,
|
||||||
opacity: 0.3,
|
opacity: 0.3,
|
||||||
},
|
},
|
||||||
@@ -804,7 +804,7 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Guest confirm modal
|
// Guest confirm modal
|
||||||
guestConfirmOverlay: {
|
guestConfirmOverlay: {
|
||||||
...StyleSheet.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { View, Text, StyleSheet, Platform, TouchableOpacity } from "react-native";
|
import { View, Text, StyleSheet, Platform, TouchableOpacity } from "react-native";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { createAudioPlayer, type AudioPlayer } from "expo-audio";
|
import { Audio } from "expo-av";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@@ -59,7 +59,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
|
|||||||
maxDurationSeconds: maxDuration,
|
maxDurationSeconds: maxDuration,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sound, setSound] = useState<AudioPlayer | null>(null);
|
const [sound, setSound] = useState<Audio.Sound | null>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [playbackPosition, setPlaybackPosition] = useState(0);
|
const [playbackPosition, setPlaybackPosition] = useState(0);
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (sound) {
|
if (sound) {
|
||||||
sound.remove();
|
sound.unloadAsync();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [sound]);
|
}, [sound]);
|
||||||
@@ -113,26 +113,30 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
|
|||||||
await triggerHaptic('light');
|
await triggerHaptic('light');
|
||||||
|
|
||||||
if (isPlaying && sound) {
|
if (isPlaying && sound) {
|
||||||
sound.pause();
|
await sound.pauseAsync();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sound) {
|
if (sound) {
|
||||||
sound.play();
|
await sound.playAsync();
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSound = createAudioPlayer({ uri: recordingUri });
|
const { sound: newSound } = await Audio.Sound.createAsync(
|
||||||
newSound.addListener('playbackStatusUpdate', (status) => {
|
{ uri: recordingUri },
|
||||||
setPlaybackPosition(status.currentTime);
|
{ shouldPlay: true },
|
||||||
if (status.didJustFinish) {
|
(status) => {
|
||||||
setIsPlaying(false);
|
if (status.isLoaded) {
|
||||||
setPlaybackPosition(0);
|
setPlaybackPosition(status.positionMillis / 1000);
|
||||||
|
if (status.didJustFinish) {
|
||||||
|
setIsPlaying(false);
|
||||||
|
setPlaybackPosition(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
newSound.play();
|
|
||||||
setSound(newSound);
|
setSound(newSound);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
};
|
};
|
||||||
@@ -141,7 +145,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
|
|||||||
if (!recordingUri) return;
|
if (!recordingUri) return;
|
||||||
await triggerHaptic('medium');
|
await triggerHaptic('medium');
|
||||||
if (sound) {
|
if (sound) {
|
||||||
sound.remove();
|
await sound.unloadAsync();
|
||||||
setSound(null);
|
setSound(null);
|
||||||
}
|
}
|
||||||
onAnswer('yes', { type: 'audio', media_path: recordingUri, duration_seconds: durationSeconds });
|
onAnswer('yes', { type: 'audio', media_path: recordingUri, duration_seconds: durationSeconds });
|
||||||
@@ -150,7 +154,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
|
|||||||
const handleReRecord = async () => {
|
const handleReRecord = async () => {
|
||||||
await triggerHaptic('light');
|
await triggerHaptic('light');
|
||||||
if (sound) {
|
if (sound) {
|
||||||
sound.remove();
|
await sound.unloadAsync();
|
||||||
setSound(null);
|
setSound(null);
|
||||||
}
|
}
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
@@ -161,7 +165,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
|
|||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
await triggerHaptic('light');
|
await triggerHaptic('light');
|
||||||
if (sound) {
|
if (sound) {
|
||||||
sound.remove();
|
await sound.unloadAsync();
|
||||||
setSound(null);
|
setSound(null);
|
||||||
}
|
}
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
@@ -172,7 +176,7 @@ export default function QuestionCardAudio({ question, packInfo, onAnswer, onRepo
|
|||||||
const handleSkip = async () => {
|
const handleSkip = async () => {
|
||||||
await triggerHaptic('light');
|
await triggerHaptic('light');
|
||||||
if (sound) {
|
if (sound) {
|
||||||
sound.remove();
|
await sound.unloadAsync();
|
||||||
setSound(null);
|
setSound(null);
|
||||||
}
|
}
|
||||||
onAnswer('skip');
|
onAnswer('skip');
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const styles = StyleSheet.create({
|
|||||||
padding: spacing.lg,
|
padding: spacing.lg,
|
||||||
},
|
},
|
||||||
backdrop: {
|
backdrop: {
|
||||||
...StyleSheet.absoluteFill,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||||
},
|
},
|
||||||
modalContainer: {
|
modalContainer: {
|
||||||
|
|||||||