Skip to content

Commit d6dac62

Browse files
Merge branch 'main' into @adamgrzybowski/fix-offline-your-spend
2 parents f7fbe84 + 05d32d5 commit d6dac62

1,035 files changed

Lines changed: 41251 additions & 15725 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/coding-standards/SKILL.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r
1313
| Category | Prefix | Focus |
1414
|----------|--------|-------|
1515
| Performance | `PERF-*` | Render optimization, memo patterns, useEffect hygiene, data selection |
16-
| Consistency | `CONSISTENCY-*` | Platform checks, magic values, unused props, ESLint discipline |
17-
| Clean React Patterns | `CLEAN-REACT-PATTERNS-*` | Composition, component ownership, state structure |
16+
| Consistency | `CONSISTENCY-*` | Platform checks, magic values, unused props, ESLint discipline, localization, file naming, JSDoc |
17+
| Clean React Patterns | `CLEAN-REACT-PATTERNS-*` | Composition, component ownership, state structure, prop typing, function components |
18+
| UI | `UI-*` | Loading indicators, scrollable pages, styling conventions |
1819

1920
## Quick Reference
2021

@@ -42,6 +43,11 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r
4243
- [CONSISTENCY-4](rules/consistency-4-no-unused-props.md) — No unused props
4344
- [CONSISTENCY-5](rules/consistency-5-justify-eslint-disable.md) — Justify ESLint disables
4445
- [CONSISTENCY-6](rules/consistency-6-proper-error-handling.md) — Proper error handling
46+
- [CONSISTENCY-7](rules/consistency-7-localize-copy.md) — Localize all user-visible copy
47+
- [CONSISTENCY-8](rules/consistency-8-localize-numbers-dates.md) — Localize numbers, amounts, dates and phone numbers
48+
- [CONSISTENCY-9](rules/consistency-9-file-naming.md) — Name files after what they export
49+
- [CONSISTENCY-10](rules/consistency-10-jsdoc.md) — Follow the JSDoc style guidelines
50+
- [CONSISTENCY-11](rules/consistency-11-no-todo-comments.md) — Track future work in an issue, not a TODO comment
4551

4652
### Clean React Patterns
4753
- [CLEAN-REACT-PATTERNS-0](rules/clean-react-0-compiler.md) — React Compiler compliance
@@ -50,6 +56,15 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r
5056
- [CLEAN-REACT-PATTERNS-3](rules/clean-react-3-context-free-contracts.md) — Context-free component contracts
5157
- [CLEAN-REACT-PATTERNS-4](rules/clean-react-4-no-side-effect-spaghetti.md) — No side-effect spaghetti
5258
- [CLEAN-REACT-PATTERNS-5](rules/clean-react-5-narrow-state.md) — Keep state narrow
59+
- [CLEAN-REACT-PATTERNS-6](rules/clean-react-6-no-componentprops.md) — Import the exported prop type instead of ComponentProps
60+
- [CLEAN-REACT-PATTERNS-7](rules/clean-react-7-no-inline-prop-types.md) — Do not inline prop types on exported components
61+
- [CLEAN-REACT-PATTERNS-8](rules/clean-react-8-no-class-components.md) — Use function components, not class components
62+
- [CLEAN-REACT-PATTERNS-9](rules/clean-react-9-no-proptypes.md) — Use TypeScript types, not propTypes or defaultProps
63+
64+
### UI
65+
- [UI-1](rules/ui-1-correct-loading-indicator.md) — Use the correct loading indicator based on navigation context
66+
- [UI-2](rules/ui-2-new-page-scrollview.md) — New pages must be scrollable
67+
- [UI-3](rules/ui-3-no-inline-styles.md) — Do not use inline style objects
5368

5469
## Usage
5570

.claude/skills/coding-standards/rules/clean-react-6-no-componentprops.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
2-
ruleId: CLEAN-REACT-6
2+
ruleId: CLEAN-REACT-PATTERNS-6
33
title: Import the exported prop type instead of ComponentProps
44
---
55

6-
## [CLEAN-REACT-6] Import the exported prop type instead of ComponentProps
6+
## [CLEAN-REACT-PATTERNS-6] Import the exported prop type instead of ComponentProps
77

88
### Reasoning
99

.claude/skills/coding-standards/rules/clean-react-7-no-inline-prop-types.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
2-
ruleId: CLEAN-REACT-7
2+
ruleId: CLEAN-REACT-PATTERNS-7
33
title: Do not inline prop types on exported components
44
---
55

6-
## [CLEAN-REACT-7] Do not inline prop types on exported components
6+
## [CLEAN-REACT-PATTERNS-7] Do not inline prop types on exported components
77

88
### Reasoning
99

.claude/skills/coding-standards/rules/clean-react-8-no-class-components.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
2-
ruleId: CLEAN-REACT-8
2+
ruleId: CLEAN-REACT-PATTERNS-8
33
title: Use function components, not class components
44
---
55

6-
## [CLEAN-REACT-8] Use function components, not class components
6+
## [CLEAN-REACT-PATTERNS-8] Use function components, not class components
77

88
### Reasoning
99

.claude/skills/coding-standards/rules/clean-react-9-no-proptypes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
2-
ruleId: CLEAN-REACT-9
2+
ruleId: CLEAN-REACT-PATTERNS-9
33
title: Use TypeScript types, not propTypes or defaultProps
44
---
55

6-
## [CLEAN-REACT-9] Use TypeScript types, not propTypes or defaultProps
6+
## [CLEAN-REACT-PATTERNS-9] Use TypeScript types, not propTypes or defaultProps
77

88
### Reasoning
99

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Upload build to Sentry for size analysis
2+
description: |
3+
Download a platform binary and upload it to Sentry for size analysis. The binary is sourced
4+
from either the current workflow run's artifacts (cherry-pick prod deploys, which build natively)
5+
or the matching staging GitHub Release (re-promotion prod deploys, which do not build natively).
6+
7+
inputs:
8+
platform:
9+
description: "Platform label used in the GH Actions notice (e.g. 'Android' or 'iOS')"
10+
required: true
11+
asset-name:
12+
description: "Filename of the binary on disk and as the GH Release asset (e.g. 'Expensify-release.aab')"
13+
required: true
14+
artifact-name:
15+
description: "Name of the GH Actions artifact to download for cherry-pick deploys (e.g. 'androidBuild-artifact')"
16+
required: true
17+
staging-release-tag:
18+
description: "Tag of the staging GH Release to download from for re-promotion deploys (e.g. '9.4.13-0-staging')"
19+
required: true
20+
is-cherry-pick:
21+
description: "'true' when this deploy is a cherry-pick (binary is in the current run); 'false' otherwise (binary is on the staging release)"
22+
required: true
23+
24+
outputs:
25+
SENTRY_URL:
26+
description: URL to the Sentry size analysis for the uploaded build (empty if upload failed or quota exhausted)
27+
value: ${{ steps.upload.outputs.SENTRY_URL }}
28+
29+
runs:
30+
using: composite
31+
steps:
32+
- name: Setup Node
33+
uses: ./.github/actions/composite/setupNode
34+
35+
- name: Download binary from current workflow run (cherry-pick path)
36+
if: ${{ inputs.is-cherry-pick == 'true' }}
37+
continue-on-error: true
38+
# v7
39+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
40+
with:
41+
name: ${{ inputs.artifact-name }}
42+
path: ./
43+
44+
- name: Download binary from the staging GitHub Release (re-promotion path)
45+
if: ${{ inputs.is-cherry-pick != 'true' }}
46+
continue-on-error: true
47+
shell: bash
48+
env:
49+
TAG: ${{ inputs.staging-release-tag }}
50+
ASSET: ${{ inputs.asset-name }}
51+
run: |
52+
gh release download "$TAG" --pattern "$ASSET" --output "$ASSET"
53+
[ -s "$ASSET" ] || { echo "::error::Failed to obtain $ASSET from $TAG"; exit 1; }
54+
55+
- name: Upload to Sentry
56+
id: upload
57+
if: ${{ hashFiles(inputs.asset-name) != '' }}
58+
continue-on-error: true
59+
shell: bash
60+
env:
61+
ASSET: ${{ inputs.asset-name }}
62+
PLATFORM: ${{ inputs.platform }}
63+
run: |
64+
OUTPUT=$(npx sentry-cli build upload "$ASSET" \
65+
--org expensify --project app --build-configuration Release --log-level info 2>&1)
66+
echo "$OUTPUT"
67+
SENTRY_URL=$(echo "$OUTPUT" | grep -oE 'https://expensify\.sentry\.io/[^ )"}]+' | head -1)
68+
if [ -n "$SENTRY_URL" ]; then
69+
echo "::notice::${PLATFORM} Sentry size analysis: $SENTRY_URL"
70+
echo "SENTRY_URL=$SENTRY_URL" >> "$GITHUB_OUTPUT"
71+
fi

.github/workflows/buildAdHoc.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
owner: context.repo.owner,
5555
repo: context.repo.repo,
5656
issue_number: ${{ inputs.APP_PR_NUMBER || 0 }},
57-
body: `🚧 @${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).`
57+
body: `🚧 ${{ github.actor }} has triggered a test Expensify/App build. You can view the [workflow run here](${workflowURL}).`
5858
});
5959
6060
- name: Add build start comment to Expensify/Mobile-Expensify PR
@@ -69,7 +69,7 @@ jobs:
6969
owner: context.repo.owner,
7070
repo: 'Mobile-Expensify',
7171
issue_number: ${{ inputs.MOBILE_EXPENSIFY_PR || 0 }},
72-
body: `🚧 @${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).`
72+
body: `🚧 ${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).`
7373
});
7474
7575
buildAndroid:

.github/workflows/buildAndroid.yml

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ on:
3535
ROCK_ARTIFACT_URL:
3636
description: URL to download the ad-hoc build artifact (adhoc only)
3737
value: ${{ jobs.build.outputs.ROCK_ARTIFACT_URL }}
38-
SENTRY_URL:
39-
description: URL to Sentry size analysis
40-
value: ${{ jobs.build.outputs.SENTRY_URL }}
4138
AAB_FILENAME:
4239
description: Filename of the AAB artifact (empty if no AAB was produced)
4340
value: ${{ jobs.build.outputs.AAB_FILENAME }}
@@ -60,7 +57,6 @@ jobs:
6057
outputs:
6158
VERSION_CODE: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
6259
ROCK_ARTIFACT_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
63-
SENTRY_URL: ${{ steps.sentry-upload.outputs.SENTRY_URL }}
6460
AAB_FILENAME: ${{ steps.collectArtifacts.outputs.AAB_FILENAME }}
6561
APK_FILENAME: ${{ steps.collectArtifacts.outputs.APK_FILENAME }}
6662
SOURCEMAP_FILENAME: index.android.bundle.map
@@ -195,22 +191,6 @@ jobs:
195191
if: ${{ inputs.variant == 'Adhoc' }}
196192
run: echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_OUTPUT"
197193

198-
- name: Upload Android build to Sentry for size analysis
199-
id: sentry-upload
200-
if: ${{ inputs.variant == 'Release' && env.ARTIFACT_PATH != '' }}
201-
continue-on-error: true
202-
timeout-minutes: 5
203-
run: |
204-
OUTPUT=$(npx sentry-cli build upload "$ARTIFACT_PATH" --org expensify --project app --build-configuration Release --log-level debug 2>&1)
205-
echo "$OUTPUT"
206-
SENTRY_URL=$(echo "$OUTPUT" | grep -oE 'https://expensify\.sentry\.io/[^ ]+' | head -1)
207-
if [ -n "$SENTRY_URL" ]; then
208-
echo "::notice::Android Sentry size analysis: $SENTRY_URL"
209-
echo "SENTRY_URL=$SENTRY_URL" >> "$GITHUB_OUTPUT"
210-
fi
211-
env:
212-
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
213-
214194
- name: Upload Gradle profile report
215195
if: always()
216196
# v6

.github/workflows/buildIOS.yml

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ on:
3535
ROCK_ARTIFACT_URL:
3636
description: URL to download the ad-hoc build artifact (adhoc only)
3737
value: ${{ jobs.build.outputs.ROCK_ARTIFACT_URL }}
38-
SENTRY_URL:
39-
description: URL to Sentry size analysis
40-
value: ${{ jobs.build.outputs.SENTRY_URL }}
4138
IPA_FILENAME:
4239
description: Filename of the IPA artifact produced by the build
4340
value: ${{ jobs.build.outputs.IPA_FILENAME }}
@@ -58,7 +55,6 @@ jobs:
5855
outputs:
5956
IOS_VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }}
6057
ROCK_ARTIFACT_URL: ${{ steps.set-artifact-url.outputs.ARTIFACT_URL }}
61-
SENTRY_URL: ${{ steps.sentry-upload.outputs.SENTRY_URL }}
6258
IPA_FILENAME: ${{ steps.set-ipa-filename.outputs.IPA_FILENAME }}
6359
DSYM_FILENAME: ${{ steps.set-ipa-filename.outputs.DSYM_FILENAME }}
6460
SOURCEMAP_FILENAME: main.jsbundle.map
@@ -244,22 +240,6 @@ jobs:
244240
comment-bot: false
245241
custom-identifier: ${{ steps.computeIdentifier.outputs.IDENTIFIER }}
246242

247-
- name: Upload iOS build to Sentry for size analysis
248-
id: sentry-upload
249-
if: ${{ inputs.variant == 'Release' && env.ARTIFACT_PATH != '' }}
250-
continue-on-error: true
251-
timeout-minutes: 5
252-
run: |
253-
OUTPUT=$(npx sentry-cli build upload "$ARTIFACT_PATH" --org expensify --project app --build-configuration Release --log-level debug 2>&1)
254-
echo "$OUTPUT"
255-
SENTRY_URL=$(echo "$OUTPUT" | grep -oE 'https://expensify\.sentry\.io/[^ ]+' | head -1)
256-
if [ -n "$SENTRY_URL" ]; then
257-
echo "::notice::iOS Sentry size analysis: $SENTRY_URL"
258-
echo "SENTRY_URL=$SENTRY_URL" >> "$GITHUB_OUTPUT"
259-
fi
260-
env:
261-
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
262-
263243
- name: Set artifact URL output
264244
id: set-artifact-url
265245
if: ${{ inputs.variant == 'Adhoc' }}

.github/workflows/deploy.yml

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,8 @@ jobs:
533533
- name: Submit previous production build to 100%
534534
run: bundle exec fastlane ios complete_hybrid_rollout
535535
continue-on-error: true
536+
env:
537+
APPLE_ID: ${{ vars.APPLE_ID }}
536538

537539
- name: Submit production build for App Store review and a slow rollout
538540
run: bundle exec fastlane ios submit_hybrid_for_rollout
@@ -916,6 +918,12 @@ jobs:
916918
--target "${{ env.DEPLOY_ENV }}"; then
917919
break
918920
fi
921+
922+
# A transient GitHub error during create doesn't necessarily mean the release failed; it may have been created anyway, so a later "already exists" error means it's present.
923+
if gh release view "${{ needs.prep.outputs.TAG }}" --repo "${{ github.repository }}" &> /dev/null; then
924+
echo "Release ${{ needs.prep.outputs.TAG }} exists despite a create error (likely a transient 502), treating as success."
925+
break
926+
fi
919927
if [[ $i -lt $CREATE_MAX_RETRIES ]]; then
920928
echo "Failed to create release. Retrying in 3 seconds... ($((CREATE_MAX_RETRIES - i)) attempts left)"
921929
sleep 3
@@ -997,6 +1005,68 @@ jobs:
9971005
}]
9981006
}
9991007
1008+
# Upload the production-bound binary to Sentry for size analysis.
1009+
# - Cherry-pick deploys build native in this run, so we pull the .aab/.ipa from the build artifact.
1010+
# - Re-promotion deploys don't build native, so we download from the matching `<TAG>-staging`
1011+
# GitHub Release populated during the staging deploy of that version.
1012+
androidUploadSentry:
1013+
name: Upload Android build to Sentry for size analysis
1014+
needs: [prep, androidBuild, checkDeploymentSuccess]
1015+
runs-on: blacksmith-2vcpu-ubuntu-2404
1016+
if: >-
1017+
${{ always()
1018+
&& needs.prep.outputs.DEPLOY_ENV == 'production'
1019+
&& fromJSON(needs.checkDeploymentSuccess.outputs.IS_RELEASE_READY) }}
1020+
continue-on-error: true
1021+
timeout-minutes: 10
1022+
outputs:
1023+
SENTRY_URL: ${{ steps.sentry-upload.outputs.SENTRY_URL }}
1024+
steps:
1025+
- name: Checkout
1026+
uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1
1027+
1028+
- name: Upload to Sentry for size analysis
1029+
id: sentry-upload
1030+
uses: ./.github/actions/composite/uploadSentrySizeAnalysis
1031+
env:
1032+
GH_TOKEN: ${{ github.token }}
1033+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
1034+
with:
1035+
platform: Android
1036+
asset-name: Expensify-release.aab
1037+
artifact-name: androidBuild-artifact
1038+
staging-release-tag: ${{ needs.prep.outputs.TAG }}-staging
1039+
is-cherry-pick: ${{ needs.prep.outputs.IS_CHERRY_PICK }}
1040+
1041+
iosUploadSentry:
1042+
name: Upload iOS build to Sentry for size analysis
1043+
needs: [prep, iosBuild, checkDeploymentSuccess]
1044+
runs-on: blacksmith-2vcpu-ubuntu-2404
1045+
if: >-
1046+
${{ always()
1047+
&& needs.prep.outputs.DEPLOY_ENV == 'production'
1048+
&& fromJSON(needs.checkDeploymentSuccess.outputs.IS_RELEASE_READY) }}
1049+
continue-on-error: true
1050+
timeout-minutes: 10
1051+
outputs:
1052+
SENTRY_URL: ${{ steps.sentry-upload.outputs.SENTRY_URL }}
1053+
steps:
1054+
- name: Checkout
1055+
uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1
1056+
1057+
- name: Upload to Sentry for size analysis
1058+
id: sentry-upload
1059+
uses: ./.github/actions/composite/uploadSentrySizeAnalysis
1060+
env:
1061+
GH_TOKEN: ${{ github.token }}
1062+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
1063+
with:
1064+
platform: iOS
1065+
asset-name: Expensify.ipa
1066+
artifact-name: iosBuild-artifact
1067+
staging-release-tag: ${{ needs.prep.outputs.TAG }}-staging
1068+
is-cherry-pick: ${{ needs.prep.outputs.IS_CHERRY_PICK }}
1069+
10001070
# Why is this necessary for CP-to-prod? Consider this scenario:
10011071
# 1. You close a checklist and we create a new staging version `9.0.34-0` and a new checklist
10021072
# 2. You then CP a PR to production, and in the process create `9.0.35-0`
@@ -1066,13 +1136,13 @@ jobs:
10661136
postGithubComments:
10671137
uses: ./.github/workflows/postDeployComments.yml
10681138
if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_RELEASE_READY) }}
1069-
needs: [prep, checkDeploymentSuccess, createRelease, androidBuild, iosBuild]
1139+
needs: [prep, checkDeploymentSuccess, createRelease, androidBuild, iosBuild, androidUploadSentry, iosUploadSentry]
10701140
secrets: inherit
10711141
with:
10721142
version: ${{ needs.prep.outputs.APP_VERSION }}
10731143
env: ${{ needs.prep.outputs.DEPLOY_ENV }}
10741144
android: ${{ needs.checkDeploymentSuccess.outputs.ANDROID_RESULT }}
10751145
ios: ${{ needs.checkDeploymentSuccess.outputs.IOS_RESULT }}
10761146
web: ${{ needs.checkDeploymentSuccess.outputs.WEB_RESULT }}
1077-
android_sentry_url: ${{ needs.androidBuild.outputs.SENTRY_URL }}
1078-
ios_sentry_url: ${{ needs.iosBuild.outputs.SENTRY_URL }}
1147+
android_sentry_url: ${{ needs.androidUploadSentry.outputs.SENTRY_URL }}
1148+
ios_sentry_url: ${{ needs.iosUploadSentry.outputs.SENTRY_URL }}

0 commit comments

Comments
 (0)