Skip to content

Commit 1b507dd

Browse files
committed
Merge branch 'main' into fix/refetch-spend-over-time-on-screen-focus
2 parents 8d5f14e + a42c40c commit 1b507dd

51 files changed

Lines changed: 807 additions & 159 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.

Mobile-Expensify

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ android {
111111
minSdkVersion rootProject.ext.minSdkVersion
112112
targetSdkVersion rootProject.ext.targetSdkVersion
113113
multiDexEnabled rootProject.ext.multiDexEnabled
114-
versionCode 1009036205
115-
versionName "9.3.62-5"
114+
versionCode 1009036206
115+
versionName "9.3.62-6"
116116
// Supported language variants must be declared here to avoid from being removed during the compilation.
117117
// This also helps us to not include unnecessary language variants in the APK.
118118
resConfigs "en", "es"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const name = 'require-a11y-disable-justification';
2+
3+
const ISSUE_URL_REGEX = /https?:\/\/github\.com\/(?:Expensify\/App|FormidableLabs\/eslint-plugin-react-native-a11y)\/(?:issues|pull)\/\d+/i;
4+
const ISSUE_URL_GLOBAL_REGEX = /https?:\/\/github\.com\/(?:Expensify\/App|FormidableLabs\/eslint-plugin-react-native-a11y)\/(?:issues|pull)\/\d+/gi;
5+
const DISABLE_A11Y_REGEX = /eslint-disable(?:-next-line|-line)?\s+[\s\S]*?react-native-a11y\//i;
6+
const MIN_RATIONALE_LENGTH = 12;
7+
8+
const meta = {
9+
type: 'problem',
10+
docs: {
11+
description: 'Require react-native-a11y eslint-disable comments to include rationale or tracking issue link.',
12+
recommended: 'error',
13+
},
14+
schema: [],
15+
messages: {
16+
missingIssueOrRationale: 'react-native-a11y eslint-disable comments must include rationale or tracking issue link.',
17+
},
18+
};
19+
20+
function normalizeComment(comment) {
21+
return comment.value
22+
.split('\n')
23+
.map((line) => line.replace(/^\s*\*+\s?/, '').trim())
24+
.join(' ')
25+
.replaceAll(/\s+/g, ' ')
26+
.trim();
27+
}
28+
29+
function hasRationale(commentText) {
30+
const strippedComment = commentText
31+
.replaceAll(/eslint-disable(?:-next-line|-line)?/gi, ' ')
32+
.replaceAll(/[a-z0-9-]+\/[a-z0-9-]+/gi, ' ')
33+
.replaceAll(ISSUE_URL_GLOBAL_REGEX, ' ')
34+
.replaceAll('--', ' ')
35+
.replaceAll(/[,*]/g, ' ')
36+
.replaceAll(/\s+/g, ' ')
37+
.trim();
38+
39+
return strippedComment.length >= MIN_RATIONALE_LENGTH;
40+
}
41+
42+
function create(context) {
43+
const sourceCode = context.getSourceCode();
44+
45+
return {
46+
Program() {
47+
for (const comment of sourceCode.getAllComments()) {
48+
const commentText = normalizeComment(comment);
49+
if (!DISABLE_A11Y_REGEX.test(commentText)) {
50+
continue;
51+
}
52+
53+
const hasIssueLink = ISSUE_URL_REGEX.test(commentText);
54+
const hasDisableRationale = hasRationale(commentText);
55+
56+
if (!hasIssueLink && !hasDisableRationale) {
57+
context.report({
58+
loc: comment.loc,
59+
messageId: 'missingIssueOrRationale',
60+
});
61+
}
62+
}
63+
},
64+
};
65+
}
66+
67+
export {name, meta, create};
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
const name = 'require-live-region-for-status-updates';
2+
3+
const STATUS_ROLES = new Set(['alert', 'status']);
4+
5+
const meta = {
6+
type: 'problem',
7+
docs: {
8+
description: 'Require role/accessibilityRole and accessibilityLiveRegion to be used together for status updates.',
9+
recommended: 'error',
10+
},
11+
schema: [],
12+
messages: {
13+
missingLiveRegion: 'Elements with role/accessibilityRole set to "alert" or "status" must include accessibilityLiveRegion so updates are announced.',
14+
missingStatusRole: 'Elements with accessibilityLiveRegion must include role/accessibilityRole set to "alert" or "status" so assistive technologies treat updates as status messages.',
15+
invalidLiveRegion: 'Use accessibilityLiveRegion="polite" or "assertive" for status updates; "none" suppresses announcements.',
16+
},
17+
};
18+
19+
function getJSXAttribute(node, names) {
20+
return node.attributes.find((attribute) => attribute.type === 'JSXAttribute' && names.includes(attribute.name?.name));
21+
}
22+
23+
function getAttributeStringValue(attribute) {
24+
if (!attribute?.value) {
25+
return null;
26+
}
27+
28+
if (attribute.value.type === 'Literal' && typeof attribute.value.value === 'string') {
29+
return attribute.value.value;
30+
}
31+
32+
if (attribute.value.type !== 'JSXExpressionContainer') {
33+
return null;
34+
}
35+
36+
if (attribute.value.expression.type === 'Literal' && typeof attribute.value.expression.value === 'string') {
37+
return attribute.value.expression.value;
38+
}
39+
40+
return null;
41+
}
42+
43+
function getConstRoleValue(attribute) {
44+
if (attribute?.value?.type !== 'JSXExpressionContainer') {
45+
return null;
46+
}
47+
48+
return getConstRoleValueFromExpression(attribute.value.expression);
49+
}
50+
51+
function getConstRoleValueFromExpression(expression) {
52+
if (expression.type !== 'MemberExpression' || expression.computed || expression.property.type !== 'Identifier') {
53+
return null;
54+
}
55+
56+
if (
57+
expression.object.type !== 'MemberExpression' ||
58+
expression.object.computed ||
59+
expression.object.object.type !== 'Identifier' ||
60+
expression.object.object.name !== 'CONST' ||
61+
expression.object.property.type !== 'Identifier' ||
62+
expression.object.property.name !== 'ROLE'
63+
) {
64+
return null;
65+
}
66+
67+
return expression.property.name.toLowerCase();
68+
}
69+
70+
function getResolvedRolesFromExpression(expression) {
71+
if (expression.type === 'Literal' && typeof expression.value === 'string') {
72+
return new Set([expression.value.toLowerCase()]);
73+
}
74+
75+
if (expression.type === 'Identifier' && expression.name === 'undefined') {
76+
return new Set();
77+
}
78+
79+
if (expression.type === 'MemberExpression') {
80+
const constRoleValue = getConstRoleValueFromExpression(expression);
81+
return constRoleValue ? new Set([constRoleValue]) : null;
82+
}
83+
84+
if (expression.type === 'ConditionalExpression') {
85+
const consequentRoles = getResolvedRolesFromExpression(expression.consequent);
86+
const alternateRoles = getResolvedRolesFromExpression(expression.alternate);
87+
88+
if (!consequentRoles || !alternateRoles) {
89+
return null;
90+
}
91+
92+
return new Set([...consequentRoles, ...alternateRoles]);
93+
}
94+
95+
if (expression.type === 'LogicalExpression' && expression.operator === '&&') {
96+
const rightRoles = getResolvedRolesFromExpression(expression.right);
97+
98+
if (!rightRoles) {
99+
return null;
100+
}
101+
102+
return rightRoles;
103+
}
104+
105+
return null;
106+
}
107+
108+
function getResolvedRoles(attribute) {
109+
if (!attribute?.value) {
110+
return new Set();
111+
}
112+
113+
if (attribute.value.type === 'Literal' && typeof attribute.value.value === 'string') {
114+
return new Set([attribute.value.value.toLowerCase()]);
115+
}
116+
117+
if (attribute.value.type !== 'JSXExpressionContainer') {
118+
return null;
119+
}
120+
121+
return getResolvedRolesFromExpression(attribute.value.expression);
122+
}
123+
124+
function getResolvedRole(attribute) {
125+
return getAttributeStringValue(attribute)?.toLowerCase() ?? getConstRoleValue(attribute);
126+
}
127+
128+
function create(context) {
129+
return {
130+
JSXOpeningElement(node) {
131+
const roleAttribute = getJSXAttribute(node, ['role', 'accessibilityRole']);
132+
const liveRegionAttribute = getJSXAttribute(node, ['accessibilityLiveRegion']);
133+
134+
const resolvedRole = getResolvedRole(roleAttribute);
135+
const resolvedRoles = getResolvedRoles(roleAttribute);
136+
const hasKnownStatusRole = STATUS_ROLES.has(resolvedRole);
137+
const hasRoleAttribute = !!roleAttribute;
138+
const hasLiveRegion = !!liveRegionAttribute;
139+
const liveRegionValue = getAttributeStringValue(liveRegionAttribute)?.toLowerCase();
140+
const hasAnyResolvedStatusRole = !!resolvedRoles && [...resolvedRoles].some((role) => STATUS_ROLES.has(role));
141+
const hasOnlyResolvedNonStatusRoles = !!resolvedRoles && resolvedRoles.size > 0 && !hasAnyResolvedStatusRole;
142+
143+
if ((hasKnownStatusRole || hasAnyResolvedStatusRole) && !hasLiveRegion) {
144+
context.report({
145+
node,
146+
messageId: 'missingLiveRegion',
147+
});
148+
return;
149+
}
150+
151+
if (hasLiveRegion && (!hasRoleAttribute || hasOnlyResolvedNonStatusRoles)) {
152+
context.report({
153+
node,
154+
messageId: 'missingStatusRole',
155+
});
156+
return;
157+
}
158+
159+
if ((hasKnownStatusRole || hasAnyResolvedStatusRole) && liveRegionValue === 'none') {
160+
context.report({
161+
node,
162+
messageId: 'invalidLiveRegion',
163+
});
164+
}
165+
},
166+
};
167+
}
168+
169+
export {name, meta, create};

eslint.config.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import jsdoc from 'eslint-plugin-jsdoc';
66
import lodash from 'eslint-plugin-lodash';
77
import react from 'eslint-plugin-react';
88
import reactNativeA11Y from 'eslint-plugin-react-native-a11y';
9+
import rulesdir from 'eslint-plugin-rulesdir';
910
import testingLibrary from 'eslint-plugin-testing-library';
1011
import youDontNeedLodashUnderscore from 'eslint-plugin-you-dont-need-lodash-underscore';
1112
import {defineConfig, globalIgnores} from 'eslint/config';
1213
import globals from 'globals';
14+
import {createRequire} from 'node:module';
1315
import path from 'node:path';
1416
import {fileURLToPath} from 'node:url';
1517
import typescriptEslint from 'typescript-eslint';
@@ -18,6 +20,12 @@ import reportNameUtilsPlugin from './eslint-plugin-report-name-utils/index.mjs';
1820

1921
const filename = fileURLToPath(import.meta.url);
2022
const dirname = path.dirname(filename);
23+
const require = createRequire(import.meta.url);
24+
const expensifyConfigDirectory = path.dirname(require.resolve('eslint-config-expensify/package.json'));
25+
const expensifyRulesDir = path.resolve(expensifyConfigDirectory, 'eslint-plugin-expensify');
26+
const localRulesDir = path.resolve(dirname, 'eslint-plugin-local-rules');
27+
28+
rulesdir.RULES_DIR = [expensifyRulesDir, localRulesDir];
2129

2230
const restrictedImportPaths = [
2331
{
@@ -170,7 +178,7 @@ const config = defineConfig([
170178
extends: new FlatCompat({baseDirectory: dirname}).extends(
171179
'airbnb-typescript',
172180
'plugin:storybook/recommended',
173-
'plugin:react-native-a11y/basic',
181+
'plugin:react-native-a11y/all',
174182
'plugin:@dword-design/import-alias/recommended',
175183
'plugin:you-dont-need-lodash-underscore/all',
176184
'prettier',
@@ -286,6 +294,8 @@ const config = defineConfig([
286294
'rulesdir/prefer-underscore-method': 'off',
287295
'rulesdir/prefer-import-module-contents': 'off',
288296
'rulesdir/no-beta-handler': 'error',
297+
'rulesdir/require-live-region-for-status-updates': 'error',
298+
'rulesdir/require-a11y-disable-justification': 'error',
289299
'rulesdir/prefer-narrow-hook-dependencies': [
290300
'error',
291301
{
@@ -299,7 +309,8 @@ const config = defineConfig([
299309
],
300310

301311
// React and React Native specific rules
302-
'react-native-a11y/has-accessibility-hint': ['off'],
312+
'react-native-a11y/has-accessibility-hint': 'off',
313+
'react-native-a11y/has-valid-accessibility-ignores-invert-colors': 'error',
303314
'react/require-default-props': 'off',
304315
'react/prop-types': 'off',
305316
'react/jsx-key': 'error',

ios/NewExpensify/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
</dict>
4545
</array>
4646
<key>CFBundleVersion</key>
47-
<string>9.3.62.5</string>
47+
<string>9.3.62.6</string>
4848
<key>FullStory</key>
4949
<dict>
5050
<key>OrgId</key>

ios/NotificationServiceExtension/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<key>CFBundleShortVersionString</key>
1414
<string>9.3.62</string>
1515
<key>CFBundleVersion</key>
16-
<string>9.3.62.5</string>
16+
<string>9.3.62.6</string>
1717
<key>NSExtension</key>
1818
<dict>
1919
<key>NSExtensionPointIdentifier</key>

ios/ShareViewController/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<key>CFBundleShortVersionString</key>
1414
<string>9.3.62</string>
1515
<key>CFBundleVersion</key>
16-
<string>9.3.62.5</string>
16+
<string>9.3.62.6</string>
1717
<key>NSExtension</key>
1818
<dict>
1919
<key>NSExtensionAttributes</key>

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "new.expensify",
3-
"version": "9.3.62-5",
3+
"version": "9.3.62-6",
44
"author": "Expensify, Inc.",
55
"homepage": "https://new.expensify.com",
66
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",

0 commit comments

Comments
 (0)