diff --git a/.github/actions/awaitStagingDeploys/index.js b/.github/actions/awaitStagingDeploys/index.js
index 314a4d03295b..c6ece6eee302 100644
--- a/.github/actions/awaitStagingDeploys/index.js
+++ b/.github/actions/awaitStagingDeploys/index.js
@@ -226,6 +226,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -295,6 +297,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -304,6 +308,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -376,6 +382,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/checkDeployBlockers/checkDeployBlockers.js b/.github/actions/checkDeployBlockers/checkDeployBlockers.js
index 89c4843cc9d3..db156651f9fe 100644
--- a/.github/actions/checkDeployBlockers/checkDeployBlockers.js
+++ b/.github/actions/checkDeployBlockers/checkDeployBlockers.js
@@ -16,7 +16,7 @@ const run = function () {
console.log('Checking for unverified PRs or unresolved deploy blockers', data);
// Check the issue description to see if there are any unfinished/un-QAed items in the checklist.
- const uncheckedBoxRegex = new RegExp(`-\\s\\[\\s]\\s(?:QA|${GithubUtils.ISSUE_OR_PULL_REQUEST_REGEX.source})`);
+ const uncheckedBoxRegex = /-\s\[\s]\s(?!Accessibility)/;
if (uncheckedBoxRegex.test(data.body)) {
console.log('An unverified PR or unresolved deploy blocker was found.');
core.setOutput('HAS_DEPLOY_BLOCKERS', true);
diff --git a/.github/actions/checkDeployBlockers/index.js b/.github/actions/checkDeployBlockers/index.js
index 51e878382f01..179dc8b4a078 100644
--- a/.github/actions/checkDeployBlockers/index.js
+++ b/.github/actions/checkDeployBlockers/index.js
@@ -26,7 +26,7 @@ const run = function () {
console.log('Checking for unverified PRs or unresolved deploy blockers', data);
// Check the issue description to see if there are any unfinished/un-QAed items in the checklist.
- const uncheckedBoxRegex = new RegExp(`-\\s\\[\\s]\\s(?:QA|${GithubUtils.ISSUE_OR_PULL_REQUEST_REGEX.source})`);
+ const uncheckedBoxRegex = /-\s\[\s]\s(?!Accessibility)/;
if (uncheckedBoxRegex.test(data.body)) {
console.log('An unverified PR or unresolved deploy blocker was found.');
core.setOutput('HAS_DEPLOY_BLOCKERS', true);
@@ -196,6 +196,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -265,6 +267,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -274,6 +278,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -346,6 +352,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js b/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js
index 56535f03b29c..43992307b912 100644
--- a/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js
+++ b/.github/actions/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js
@@ -85,6 +85,7 @@ const run = function () {
// If we aren't sent a tag, then use the existing tag
const tag = newVersion || currentStagingDeployCashData.tag;
+ const didVersionChange = newVersion ? newVersion !== currentStagingDeployCashData.tag : false;
// Find the list of PRs merged between the last StagingDeployCash and the new version
const mergedPRs = GitUtils.getPullRequestsMergedBetween(previousStagingDeployCashData.tag, tag);
@@ -128,6 +129,8 @@ const run = function () {
_.pluck(_.where(PRList, {isAccessible: true}), 'url'),
_.pluck(deployBlockers, 'url'),
_.pluck(_.where(deployBlockers, {isResolved: true}), 'url'),
+ didVersionChange ? false : currentStagingDeployCashData.isTimingDashboardChecked,
+ didVersionChange ? false : currentStagingDeployCashData.isFirebaseChecked,
);
})
.then((body) => {
diff --git a/.github/actions/createOrUpdateStagingDeploy/index.js b/.github/actions/createOrUpdateStagingDeploy/index.js
index 58ecf86f7e9d..43fda09543d8 100644
--- a/.github/actions/createOrUpdateStagingDeploy/index.js
+++ b/.github/actions/createOrUpdateStagingDeploy/index.js
@@ -95,6 +95,7 @@ const run = function () {
// If we aren't sent a tag, then use the existing tag
const tag = newVersion || currentStagingDeployCashData.tag;
+ const didVersionChange = newVersion ? newVersion !== currentStagingDeployCashData.tag : false;
// Find the list of PRs merged between the last StagingDeployCash and the new version
const mergedPRs = GitUtils.getPullRequestsMergedBetween(previousStagingDeployCashData.tag, tag);
@@ -138,6 +139,8 @@ const run = function () {
_.pluck(_.where(PRList, {isAccessible: true}), 'url'),
_.pluck(deployBlockers, 'url'),
_.pluck(_.where(deployBlockers, {isResolved: true}), 'url'),
+ didVersionChange ? false : currentStagingDeployCashData.isTimingDashboardChecked,
+ didVersionChange ? false : currentStagingDeployCashData.isFirebaseChecked,
);
})
.then((body) => {
@@ -386,6 +389,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -455,6 +460,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -464,6 +471,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -536,6 +545,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/getPullRequestDetails/index.js b/.github/actions/getPullRequestDetails/index.js
index 0e8fcb725620..8ef5d2f04ebf 100644
--- a/.github/actions/getPullRequestDetails/index.js
+++ b/.github/actions/getPullRequestDetails/index.js
@@ -267,6 +267,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -336,6 +338,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -345,6 +349,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -417,6 +423,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/getReleaseBody/index.js b/.github/actions/getReleaseBody/index.js
index c8c7a0d153d3..a72d730563ff 100644
--- a/.github/actions/getReleaseBody/index.js
+++ b/.github/actions/getReleaseBody/index.js
@@ -185,6 +185,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -254,6 +256,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -263,6 +267,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -335,6 +341,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/isPullRequestMergeable/index.js b/.github/actions/isPullRequestMergeable/index.js
index 3e98b961c4f7..b03366d0c9dd 100644
--- a/.github/actions/isPullRequestMergeable/index.js
+++ b/.github/actions/isPullRequestMergeable/index.js
@@ -186,6 +186,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -255,6 +257,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -264,6 +268,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -336,6 +342,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/isStagingDeployLocked/index.js b/.github/actions/isStagingDeployLocked/index.js
index 4ef5e5aa3c6e..a0e47bbe0662 100644
--- a/.github/actions/isStagingDeployLocked/index.js
+++ b/.github/actions/isStagingDeployLocked/index.js
@@ -149,6 +149,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -218,6 +220,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -227,6 +231,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -299,6 +305,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/markPullRequestsAsDeployed/index.js b/.github/actions/markPullRequestsAsDeployed/index.js
index 1ad199a5f4cf..4f881fae56c4 100644
--- a/.github/actions/markPullRequestsAsDeployed/index.js
+++ b/.github/actions/markPullRequestsAsDeployed/index.js
@@ -318,6 +318,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -387,6 +389,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -396,6 +400,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -468,6 +474,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/reopenIssueWithComment/index.js b/.github/actions/reopenIssueWithComment/index.js
index 42d09f1c766e..7c0219f6e2b3 100644
--- a/.github/actions/reopenIssueWithComment/index.js
+++ b/.github/actions/reopenIssueWithComment/index.js
@@ -160,6 +160,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -229,6 +231,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -238,6 +242,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -310,6 +316,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/triggerWorkflowAndWait/index.js b/.github/actions/triggerWorkflowAndWait/index.js
index 86a6f5039af5..a04038faa3c0 100644
--- a/.github/actions/triggerWorkflowAndWait/index.js
+++ b/.github/actions/triggerWorkflowAndWait/index.js
@@ -329,6 +329,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -398,6 +400,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -407,6 +411,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -479,6 +485,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/actions/verifySignedCommits/index.js b/.github/actions/verifySignedCommits/index.js
index 8060ed706572..406598e3db99 100644
--- a/.github/actions/verifySignedCommits/index.js
+++ b/.github/actions/verifySignedCommits/index.js
@@ -149,6 +149,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -218,6 +220,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -227,6 +231,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -299,6 +305,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js
index be5fe177c54a..220407dd1f2f 100644
--- a/.github/libs/GithubUtils.js
+++ b/.github/libs/GithubUtils.js
@@ -109,6 +109,8 @@ class GithubUtils {
labels: issue.labels,
PRList: this.getStagingDeployCashPRList(issue),
deployBlockers: this.getStagingDeployCashDeployBlockers(issue),
+ isTimingDashboardChecked: /-\s\[x]\sI checked the \[App Timing Dashboard]/.test(issue.body),
+ isFirebaseChecked: /-\s\[x]\sI checked \[Firebase Crashlytics]/.test(issue.body),
tag,
};
} catch (exception) {
@@ -178,6 +180,8 @@ class GithubUtils {
* @param {Array} [accessiblePRList] - The list of PR URLs which have passed the accessability check.
* @param {Array} [deployBlockers] - The list of DeployBlocker URLs.
* @param {Array} [resolvedDeployBlockers] - The list of DeployBlockers URLs which have been resolved.
+ * @param {Boolean} [isTimingDashboardChecked]
+ * @param {Boolean} [isFirebaseChecked]
* @returns {Promise}
*/
static generateStagingDeployCashBody(
@@ -187,6 +191,8 @@ class GithubUtils {
accessiblePRList = [],
deployBlockers = [],
resolvedDeployBlockers = [],
+ isTimingDashboardChecked = false,
+ isFirebaseChecked = false,
) {
return this.fetchAllPullRequests(_.map(PRList, this.getPullRequestNumberFromURL))
.then((data) => {
@@ -259,6 +265,12 @@ class GithubUtils {
});
}
+ issueBody += '\r\n\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isTimingDashboardChecked ? 'x' : ' '}] I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.`;
+ // eslint-disable-next-line max-len
+ issueBody += `\r\n- [${isFirebaseChecked ? 'x' : ' '}] I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.`;
+
issueBody += '\r\n\r\ncc @Expensify/applauseleads\r\n';
return issueBody;
})
diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml
index 407a16bb7464..350380aed2b9 100644
--- a/.github/workflows/cherryPick.yml
+++ b/.github/workflows/cherryPick.yml
@@ -203,7 +203,7 @@ jobs:
PULL_REQUEST_NUMBER: ${{ steps.createPullRequest.outputs.pr_number }}
- name: Auto-merge the PR
- # Important: only auto-merge if there was no merge conflict!
+ # Important: only auto-merge if there was no merge conflict and the PR is mergable (not blocked by a missing status check)!
if: ${{ fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) && fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }}
run: gh pr merge --merge --delete-branch
env:
@@ -211,7 +211,7 @@ jobs:
- name: 'Announces a CP failure in the #announce Slack room'
uses: 8398a7/action-slack@v3
- if: ${{ failure() }}
+ if: ${{ failure() || !fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }}
with:
status: custom
custom_payload: |
diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml
index fe0eb8aff88a..8553f9b63fbb 100644
--- a/.github/workflows/finishReleaseCycle.yml
+++ b/.github/workflows/finishReleaseCycle.yml
@@ -47,7 +47,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
COMMENT: |
- This issue either has unchecked QA steps or has not yet been marked with the `:shipit:` emoji of approval.
+ This issue either has unchecked items or has not yet been marked with the `:shipit:` emoji of approval.
Reopening!
# Update the production branch to trigger the production deploy.
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index c0c9fe98676d..608aa21204e1 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -10,7 +10,7 @@ on:
env:
SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }}
- DEVELOPER_DIR: /Applications/Xcode_12.5.1.app/Contents/Developer
+ DEVELOPER_DIR: /Applications/Xcode_13.2.1.app/Contents/Developer
jobs:
validateActor:
diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml
index 85ad459978f6..266f998d4f03 100644
--- a/.github/workflows/preDeploy.yml
+++ b/.github/workflows/preDeploy.yml
@@ -216,12 +216,18 @@ jobs:
IS_EXPENSIFY_EMPLOYEE: ${{ fromJSON(steps.checkActor.outputs.isTeamMember) }}
steps:
+ - name: Get merged pull request
+ id: getMergedPullRequest
+ uses: roryabraham/action-get-merged-pull-request@7a7a194f6ff8f3eef58c822083695a97314ebec1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+
- name: Check whether the actor is member of Expensify/expensify team
id: checkActor
uses: tspascoal/get-user-teams-membership@baf2e6adf4c3b897bd65a7e3184305c165aec872
with:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
- username: ${{ github.actor }}
+ username: ${{ steps.getMergedPullRequest.outputs.author }}
team: Expensify/expensify
newContributorWelcomeMessage:
diff --git a/.storybook/main.js b/.storybook/main.js
index b229d828bfe8..ad5effb2dbf6 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -6,6 +6,7 @@ module.exports = {
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
+ '@storybook/addon-react-native-web',
],
staticDirs: [
'./public',
diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association
index a026ecac4ca8..b342834fc596 100644
--- a/.well-known/apple-app-site-association
+++ b/.well-known/apple-app-site-association
@@ -39,6 +39,10 @@
{
"/": "/partners/plaid/oauth_ios",
"comment": "Plaid setup"
+ },
+ {
+ "/": "/statements/*",
+ "comment": "Wallet statements"
}
]
}
diff --git a/FORMS.md b/FORMS.md
index 8fc47b8a9b17..b526ce5e50f5 100644
--- a/FORMS.md
+++ b/FORMS.md
@@ -21,15 +21,19 @@ Labels and hints are enabled by passing the appropriate props to each input:
### Character Limits
-If a field has a character limit we should give that field a max limit and let the user know how many characters there are left outside of the input and below it. This is done by passing the maxLength prop to TextInput.
+If a field has a character limit we should give that field a max limit. This is done by passing the maxLength prop to TextInput.
```jsx
```
+Note: We shouldn't place a max limit on a field if the entered value can be formatted. eg: Phone number.
+The phone number can be formatted in different ways.
-
+- 2109400803
+- +12109400803
+- (210)-940-0803
### Native Keyboards
@@ -66,7 +70,9 @@ All forms should define an order in which the inputs should be filled out, and u
3. Add an event listener to the page/component we are creating and update the tab index state on tab/shift + tab key press
4. Set focus to the input with that tab index.
-Additionally, ressing the enter key on any focused field should submit the form.
+Additionally, pressing the enter key on any focused field should submit the form.
+
+Note: This doesn't apply to the multiline fields. To keep the browser behavior consistent, pressing enter on the multiline should not be intercepted. It should follow the default browser behavior (such as adding a newline).
### Modifying User Input on Change
diff --git a/README.md b/README.md
index ce4e9d0d063e..b7d5146c2c0f 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@ This is a persistent storage solution wrapped in a Pub/Sub library. In general t
- Collections of data are usually not stored as a single key (eg. an array with multiple objects), but as individual keys+ID (eg. `report_1234`, `report_4567`, etc.). Store collections as individual keys when a component will bind directly to one of those keys. For example: reports are stored as individual keys because `OptionRow.js` binds to the individual report keys for each link. However, report actions are stored as an array of objects because nothing binds directly to a single report action.
- Onyx allows other code to subscribe to changes in data, and then publishes change events whenever data is changed
- Anything needing to read Onyx data needs to:
- 1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > local storage)
+ 1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > IndexedDB > OnyxDB > keyvaluepairs)
2. Subscribe to changes of the data for a particular key or set of keys. React components use `withOnyx()` and non-React libs use `Onyx.connect()`.
3. Get initialized with the current value of that key from persistent storage (Onyx does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process)
- Subscribing to Onyx keys is done using a constant defined in `ONYXKEYS`. Each Onyx key represents either a collection of items or a specific entry in storage. For example, since all reports are stored as individual keys like `report_1234`, if code needs to know about all the reports (eg. display a list of them in the nav menu), then it would subscribe to the key `ONYXKEYS.COLLECTION.REPORT`.
diff --git a/__mocks__/@react-native-community/netinfo.js b/__mocks__/@react-native-community/netinfo.js
index e6a61dcab80c..52bdd32ce786 100644
--- a/__mocks__/@react-native-community/netinfo.js
+++ b/__mocks__/@react-native-community/netinfo.js
@@ -1,6 +1,3 @@
-export default {
- configure: () => {},
- fetch: () => {},
- addEventListener: () => {},
- useNetInfo: () => {},
-};
+import NetInfoMock from '@react-native-community/netinfo/jest/netinfo-mock';
+
+export default NetInfoMock;
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4db5b8484aee..594521c50e58 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -152,8 +152,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001014901
- versionName "1.1.49-1"
+ versionCode 1001015703
+ versionName "1.1.57-3"
}
splits {
abi {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2185ffc6b3d0..912aa6d5a9b0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -68,6 +68,7 @@
+
@@ -78,6 +79,7 @@
+
diff --git a/assets/images/connect.svg b/assets/images/connect.svg
new file mode 100644
index 000000000000..6d4e9eb21d97
--- /dev/null
+++ b/assets/images/connect.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index c64aa11abc37..e07f89d62177 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.1.49
+ 1.1.57
CFBundleSignature
????
CFBundleURLTypes
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.1.49.1
+ 1.1.57.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 24e64feb7997..69f882690513 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.1.49
+ 1.1.57
CFBundleSignature
????
CFBundleVersion
- 1.1.49.1
+ 1.1.57.3
diff --git a/package-lock.json b/package-lock.json
index 432e83ff16ec..aff74f152689 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.1.49-1",
+ "version": "1.1.57-3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -6663,9 +6663,9 @@
"dev": true
},
"@react-native-community/netinfo": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-8.0.0.tgz",
- "integrity": "sha512-8cjkbOWe55vzzc64hfjDv6GWSY8+kfEnxRbwTf9l3hFYDIUMRmMoW+SwxE+QoAfMY32nbEERDy68iev3busRFQ=="
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-8.3.0.tgz",
+ "integrity": "sha512-VlmjD7Vg1BacbNhwuJCel1eeD8N2Ps6BEcZe9qoSoeIptpCbC86o4ZqD0meSjJzioKSvgalrkmPgMaVYsVipKw=="
},
"@react-native-community/progress-bar-android": {
"version": "1.0.4",
@@ -6710,9 +6710,9 @@
"integrity": "sha512-2Y9OXWHRutYmdyW+bNMaFTW8uTBpaaK20xdIFoVtqahEOO9++B+ut3CAWKPZcdRtb9ikG0LUKChGqMeocg0PGA=="
},
"@react-native-picker/picker": {
- "version": "1.9.11",
- "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-1.9.11.tgz",
- "integrity": "sha512-E7whvvMIl9Ln1sxgug7OAEVWQ69n82iV0d2OWWp5VV52+RW3azKh1IFm4rxdW5/oByMfl7FFL0eHNelGgY4BMQ=="
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.3.1.tgz",
+ "integrity": "sha512-DAw1o3bHNRnQPImsK53xCYkgC8bH+t9uTM+0JjNIlmMwvVLFnmDxi9v5iBTIWFMWG/pUHQaw66E29wW6oJG9Tw=="
},
"@react-native/assets": {
"version": "1.0.0",
@@ -8454,6 +8454,12 @@
}
}
},
+ "@storybook/addon-react-native-web": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-react-native-web/-/addon-react-native-web-0.0.18.tgz",
+ "integrity": "sha512-tMZumiF+Dgk7sngMWFrbDdBN3h5C001traYM46CqNQvtJoZvwYe2MPGEeWy3wk+5D8vOv8TFw2i96f3wwfralg==",
+ "dev": true
+ },
"@storybook/addon-toolbars": {
"version": "6.4.12",
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.4.12.tgz",
@@ -23768,8 +23774,8 @@
}
},
"expensify-common": {
- "version": "git+https://github.com/Expensify/expensify-common.git#f77bb4710c13d01153716df7fb087b637ba3b8bd",
- "from": "git+https://github.com/Expensify/expensify-common.git#f77bb4710c13d01153716df7fb087b637ba3b8bd",
+ "version": "git+https://github.com/Expensify/expensify-common.git#15237e012770048e8413dc08d4c36bc12304f3e8",
+ "from": "git+https://github.com/Expensify/expensify-common.git#15237e012770048e8413dc08d4c36bc12304f3e8",
"requires": {
"classnames": "2.3.1",
"clipboard": "2.0.4",
@@ -23789,14 +23795,6 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
- "lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "requires": {
- "yallist": "^4.0.0"
- }
- },
"react": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
@@ -23819,17 +23817,12 @@
}
},
"semver": {
- "version": "7.3.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
- "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "version": "7.3.7",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+ "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"requires": {
"lru-cache": "^6.0.0"
}
- },
- "yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
},
@@ -30719,6 +30712,14 @@
"highlight.js": "~10.7.0"
}
},
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
"make-cancellable-promise": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.0.0.tgz",
@@ -35556,9 +35557,9 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"pusher-js": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.0.0.tgz",
- "integrity": "sha512-2ZSw8msMe6EKNTebQSthRInrWUK9bo3zXPmQx0bfeDFJdSnTWUROhdAhmpRQREHzqrL+l4imv/3uwgIQHUO0oQ==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.0.6.tgz",
+ "integrity": "sha512-I44FTlF2OfGNg/4xcxmFq/JqFzJswoQWtWCPq+DkCh31MFg3Qkm3bNFvTXU+c5KR19TyBZ9SYlYq2rrpJZzbIA==",
"requires": {
"tweetnacl": "^1.0.3"
}
@@ -43076,6 +43077,11 @@
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
"yaml": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
diff --git a/package.json b/package.json
index b5fcca5f5a4b..016aafef395c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.1.49-1",
+ "version": "1.1.57-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -42,7 +42,7 @@
"@react-native-community/cli": "6.2.0",
"@react-native-community/clipboard": "^1.5.1",
"@react-native-community/datetimepicker": "^3.5.2",
- "@react-native-community/netinfo": "^8.0.0",
+ "@react-native-community/netinfo": "^8.3.0",
"@react-native-community/progress-bar-android": "^1.0.4",
"@react-native-community/progress-view": "^1.2.3",
"@react-native-firebase/analytics": "^12.3.0",
@@ -50,7 +50,7 @@
"@react-native-firebase/crashlytics": "^12.3.0",
"@react-native-firebase/perf": "^12.3.0",
"@react-native-masked-view/masked-view": "^0.2.4",
- "@react-native-picker/picker": "^1.9.11",
+ "@react-native-picker/picker": "^2.3.1",
"@react-navigation/compat": "^5.3.20",
"@react-navigation/drawer": "6.1.8",
"@react-navigation/native": "6.0.8",
@@ -59,7 +59,7 @@
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
"dotenv": "^8.2.0",
- "expensify-common": "git+https://github.com/Expensify/expensify-common.git#f77bb4710c13d01153716df7fb087b637ba3b8bd",
+ "expensify-common": "git+https://github.com/Expensify/expensify-common.git#15237e012770048e8413dc08d4c36bc12304f3e8",
"fbjs": "^3.0.2",
"file-loader": "^6.0.0",
"html-entities": "^1.3.1",
@@ -71,7 +71,7 @@
"moment-timezone": "^0.5.31",
"onfido-sdk-ui": "^6.15.2",
"prop-types": "^15.7.2",
- "pusher-js": "^7.0.0",
+ "pusher-js": "^7.0.6",
"react": "^17.0.2",
"react-collapse": "^5.1.0",
"react-dom": "^17.0.2",
@@ -126,6 +126,7 @@
"@react-native-community/eslint-config": "^2.0.0",
"@storybook/addon-a11y": "^6.4.12",
"@storybook/addon-essentials": "^6.4.12",
+ "@storybook/addon-react-native-web": "0.0.18",
"@storybook/addons": "^6.4.12",
"@storybook/react": "^6.4.12",
"@storybook/theming": "^6.4.12",
diff --git a/src/CONST.js b/src/CONST.js
index a9c0531d894e..b12b70e4a37c 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -305,17 +305,42 @@ const CONST = {
COLD: 'cold',
REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500,
TOOLTIP_SENSE: 1000,
+ SPINNER_TIMEOUT: 15 * 1000,
},
PRIORITY_MODE: {
GSD: 'gsd',
DEFAULT: 'default',
},
+ JSON_CODE: {
+ SUCCESS: 200,
+ NOT_AUTHENTICATED: 407,
+ EXP_ERROR: 666,
+ },
ERROR: {
+ XHR_FAILED: 'xhrFailed',
API_OFFLINE: 'session.offlineMessageRetry',
UNKNOWN_ERROR: 'Unknown error',
REQUEST_CANCELLED: 'AbortError',
FAILED_TO_FETCH: 'Failed to fetch',
ENSURE_BUGBOT: 'ENSURE_BUGBOT',
+ PUSHER_ERROR: 'PusherError',
+ WEB_SOCKET_ERROR: 'WebSocketError',
+ NETWORK_REQUEST_FAILED: 'Network request failed',
+ SAFARI_DOCUMENT_LOAD_ABORTED: 'cancelled',
+ FIREFOX_DOCUMENT_LOAD_ABORTED: 'NetworkError when attempting to fetch resource.',
+ IOS_NETWORK_CONNECTION_LOST: 'The network connection was lost.',
+ IOS_NETWORK_CONNECTION_LOST_RUSSIAN: 'Сетевое соединение потеряно.',
+ IOS_NETWORK_CONNECTION_LOST_SWEDISH: 'Nätverksanslutningen förlorades.',
+ IOS_LOAD_FAILED: 'Load failed',
+ SAFARI_CANNOT_PARSE_RESPONSE: 'cannot parse response',
+ GATEWAY_TIMEOUT: 'Gateway Timeout',
+ EXPENSIFY_SERVICE_INTERRUPTED: 'Expensify service interrupted',
+ },
+ ERROR_TYPE: {
+ SOCKET: 'Expensify\\Auth\\Error\\Socket',
+ },
+ ERROR_TITLE: {
+ SOCKET: 'Issue connecting to database',
},
NETWORK: {
METHOD: {
@@ -325,10 +350,6 @@ const CONST = {
PROCESS_REQUEST_DELAY_MS: 1000,
MAX_PENDING_TIME_MS: 10 * 1000,
},
- JSON_CODE: {
- SUCCESS: 200,
- NOT_AUTHENTICATED: 407,
- },
NVP: {
IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'isFirstTimeNewExpensifyUser',
BLOCKED_FROM_CONCIERGE: 'private_blockedFromConcierge',
@@ -381,6 +402,8 @@ const CONST = {
DECIMAL_PAD: 'decimal-pad',
},
+ ATTACHMENT_SOURCE_ATTRIBUTE: 'data-expensify-source',
+
ATTACHMENT_PICKER_TYPE: {
FILE: 'file',
IMAGE: 'image',
@@ -396,7 +419,7 @@ const CONST = {
EMOJI_PICKER_ITEM_HEIGHT: 40,
EMOJI_PICKER_HEADER_HEIGHT: 38,
- COMPOSER_MAX_HEIGHT: 116,
+ COMPOSER_MAX_HEIGHT: 125,
EMAIL: {
CONCIERGE: 'concierge@expensify.com',
@@ -656,6 +679,9 @@ const CONST = {
this.EMAIL.ADMIN,
];
},
+
+ // There's a limit of 60k characters in Auth - https://github.com/Expensify/Auth/blob/198d59547f71fdee8121325e8bc9241fc9c3236a/auth/lib/Request.h#L28
+ MAX_COMMENT_LENGTH: 60000,
};
export default CONST;
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index f3837f8ad61d..39d1a8f9dd09 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -243,7 +243,7 @@ class AddPlaidBankAccount extends React.Component {
{
+ onInputChange={(index) => {
this.setState({selectedIndex: Number(index)});
this.clearError('selectedBank');
}}
diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js
index 9186d3659b4c..a8d069e77cb6 100644
--- a/src/components/AddressSearch.js
+++ b/src/components/AddressSearch.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
import React, {useRef, useState} from 'react';
import PropTypes from 'prop-types';
-import {LogBox} from 'react-native';
+import {LogBox, ScrollView, View} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
import CONFIG from '../CONFIG';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
@@ -44,7 +44,7 @@ const propTypes = {
value: PropTypes.string,
/** A callback function when the value of this field has changed */
- onChange: PropTypes.func.isRequired,
+ onInputChange: PropTypes.func.isRequired,
/** Customize the TextInput container */
containerStyles: PropTypes.arrayOf(PropTypes.object),
@@ -113,93 +113,108 @@ const AddressSearch = (props) => {
if (_.size(values) === 0) {
return;
}
- props.onChange(values);
+ props.onInputChange(values);
};
return (
- {
- saveLocationDetails(details);
-
- // After we select an option, we set displayListViewBorder to false to prevent UI flickering
- setDisplayListViewBorder(false);
- }}
- query={{
- key: 'AIzaSyC4axhhXtpiS-WozJEsmlL3Kg3kXucbZus',
- language: props.preferredLocale,
- types: 'address',
- components: 'country:us',
- }}
- requestUrl={{
- useOnPlatform: 'web',
- url: `${CONFIG.EXPENSIFY.EXPENSIFY_URL}api?command=Proxy_GooglePlaces&proxyUrl=`,
- }}
- textInputProps={{
- InputComp: TextInput,
- ref: (node) => {
- if (!props.innerRef) {
- return;
- }
-
- if (_.isFunction(props.innerRef)) {
- props.innerRef(node);
- return;
- }
-
- // eslint-disable-next-line no-param-reassign
- props.innerRef.current = node;
- },
- label: props.label,
- containerStyles: props.containerStyles,
- errorText: props.errorText,
- value: props.value,
- isFormInput: props.isFormInput,
- inputID: props.inputID,
- shouldSaveDraft: props.shouldSaveDraft,
- onBlur: props.onBlur,
- autoComplete: 'off',
- onChangeText: (text) => {
- if (skippedFirstOnChangeTextRef.current) {
- props.onChange({street: text});
- } else {
- skippedFirstOnChangeTextRef.current = true;
- }
-
- // If the text is empty, we set displayListViewBorder to false to prevent UI flickering
- if (_.isEmpty(text)) {
+
+ /*
+ * The GooglePlacesAutocomplete component uses a VirtualizedList internally,
+ * and VirtualizedLists cannot be directly nested within other VirtualizedLists of the same orientation.
+ * To work around this, we wrap the GooglePlacesAutocomplete component with a horizontal ScrollView
+ * that has scrolling disabled and would otherwise not be needed
+ */
+
+
+ {
+ saveLocationDetails(details);
+
+ // After we select an option, we set displayListViewBorder to false to prevent UI flickering
setDisplayListViewBorder(false);
- }
- },
- }}
- styles={{
- textInputContainer: [styles.flexColumn],
- listView: [
- !displayListViewBorder && styles.googleListView,
- displayListViewBorder && styles.borderTopRounded,
- displayListViewBorder && styles.borderBottomRounded,
- displayListViewBorder && styles.mt1,
- styles.overflowAuto,
- styles.borderLeft,
- styles.borderRight,
- ],
- row: [
- styles.pv4,
- styles.ph3,
- styles.overflowAuto,
- ],
- description: [styles.googleSearchText],
- separator: [styles.googleSearchSeparator],
- }}
- onLayout={(event) => {
- // We use the height of the element to determine if we should hide the border of the listView dropdown
- // to prevent a lingering border when there are no address suggestions.
- // The height of the empty element is 2px (1px height for each top and bottom borders)
- setDisplayListViewBorder(event.nativeEvent.layout.height > 2);
- }}
- />
+ }}
+ query={{
+ key: 'AIzaSyC4axhhXtpiS-WozJEsmlL3Kg3kXucbZus',
+ language: props.preferredLocale,
+ types: 'address',
+ components: 'country:us',
+ }}
+ requestUrl={{
+ useOnPlatform: 'web',
+ url: `${CONFIG.EXPENSIFY.EXPENSIFY_URL}api?command=Proxy_GooglePlaces&proxyUrl=`,
+ }}
+ textInputProps={{
+ InputComp: TextInput,
+ ref: (node) => {
+ if (!props.innerRef) {
+ return;
+ }
+
+ if (_.isFunction(props.innerRef)) {
+ props.innerRef(node);
+ return;
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ props.innerRef.current = node;
+ },
+ label: props.label,
+ containerStyles: props.containerStyles,
+ errorText: props.errorText,
+ value: props.value,
+ isFormInput: props.isFormInput,
+ inputID: props.inputID,
+ shouldSaveDraft: props.shouldSaveDraft,
+ onBlur: props.onBlur,
+ autoComplete: 'off',
+ onChangeText: (text) => {
+ if (skippedFirstOnChangeTextRef.current) {
+ props.onInputChange({street: text});
+ } else {
+ skippedFirstOnChangeTextRef.current = true;
+ }
+
+ // If the text is empty, we set displayListViewBorder to false to prevent UI flickering
+ if (_.isEmpty(text)) {
+ setDisplayListViewBorder(false);
+ }
+ },
+ }}
+ styles={{
+ textInputContainer: [styles.flexColumn],
+ listView: [
+ !displayListViewBorder && styles.googleListView,
+ displayListViewBorder && styles.borderTopRounded,
+ displayListViewBorder && styles.borderBottomRounded,
+ displayListViewBorder && styles.mt1,
+ styles.overflowAuto,
+ styles.borderLeft,
+ styles.borderRight,
+ ],
+ row: [
+ styles.pv4,
+ styles.ph3,
+ styles.overflowAuto,
+ ],
+ description: [styles.googleSearchText],
+ separator: [styles.googleSearchSeparator],
+ }}
+ onLayout={(event) => {
+ // We use the height of the element to determine if we should hide the border of the listView dropdown
+ // to prevent a lingering border when there are no address suggestions.
+ // The height of the empty element is 2px (1px height for each top and bottom borders)
+ setDisplayListViewBorder(event.nativeEvent.layout.height > 2);
+ }}
+ />
+
+
);
};
diff --git a/src/components/ArchivedReportFooter.js b/src/components/ArchivedReportFooter.js
index d423fa0d4e22..cafc106ce2a4 100644
--- a/src/components/ArchivedReportFooter.js
+++ b/src/components/ArchivedReportFooter.js
@@ -8,6 +8,7 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize';
import compose from '../libs/compose';
import personalDetailsPropType from '../pages/personalDetailsPropType';
import ONYXKEYS from '../ONYXKEYS';
+import * as ReportUtils from '../libs/reportUtils';
const propTypes = {
/** The reason this report was archived */
@@ -53,7 +54,6 @@ const defaultProps = {
const ArchivedReportFooter = (props) => {
const archiveReason = lodashGet(props.reportClosedAction, 'originalMessage.reason', CONST.REPORT.ARCHIVE_REASON.DEFAULT);
- const policyName = lodashGet(props.policies, `${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}.name`);
let displayName = lodashGet(props.personalDetails, `${props.report.ownerEmail}.displayName`, props.report.ownerEmail);
let oldDisplayName;
@@ -69,7 +69,7 @@ const ArchivedReportFooter = (props) => {
text={props.translate(`reportArchiveReasons.${archiveReason}`, {
displayName: `${displayName}`,
oldDisplayName: `${oldDisplayName}`,
- policyName: `${policyName}`,
+ policyName: `${ReportUtils.getPolicyName(props.report, props.policies)}`,
})}
shouldRenderHTML={archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT}
/>
diff --git a/src/components/Button.js b/src/components/Button.js
index d192c43f7738..0857d839b617 100644
--- a/src/components/Button.js
+++ b/src/components/Button.js
@@ -10,14 +10,28 @@ import Icon from './Icon';
import CONST from '../CONST';
import * as StyleUtils from '../styles/StyleUtils';
import HapticFeedback from '../libs/HapticFeedback';
+import * as Expensicons from './Icon/Expensicons';
+import colors from '../styles/colors';
const propTypes = {
/** The text for the button label */
text: PropTypes.string,
+ /** Boolean whether to display the right icon */
+ shouldShowRightIcon: PropTypes.bool,
+
/** The icon asset to display to the left of the text */
icon: PropTypes.func,
+ /** The icon asset to display to the right of the text */
+ iconRight: PropTypes.func,
+
+ /** The fill color to pass into the icon. */
+ iconFill: PropTypes.string,
+
+ /** Any additional styles to pass to the icon container. */
+ iconStyles: PropTypes.arrayOf(PropTypes.object),
+
/** Small sized button */
small: PropTypes.bool,
@@ -27,6 +41,9 @@ const propTypes = {
/** medium sized button */
medium: PropTypes.bool,
+ /** Extra large sized button */
+ extraLarge: PropTypes.bool,
+
/** Indicates whether the button should be disabled and in the loading state */
isLoading: PropTypes.bool,
@@ -84,12 +101,17 @@ const propTypes = {
const defaultProps = {
text: '',
+ shouldShowRightIcon: false,
icon: null,
+ iconRight: Expensicons.ArrowRight,
+ iconFill: colors.white,
+ iconStyles: [],
isLoading: false,
isDisabled: false,
small: false,
large: false,
medium: false,
+ extraLarge: false,
onPress: () => {},
onLongPress: () => {},
onPressIn: () => {},
@@ -158,6 +180,7 @@ class Button extends Component {
this.props.small && styles.buttonSmallText,
this.props.medium && styles.buttonMediumText,
this.props.large && styles.buttonLargeText,
+ this.props.extraLarge && styles.buttonExtraLargeText,
this.props.success && styles.buttonSuccessText,
this.props.danger && styles.buttonDangerText,
...this.props.textStyles,
@@ -169,15 +192,29 @@ class Button extends Component {
if (this.props.icon) {
return (
-
-
-
+
+
+
+
+
+ {textComponent}
- {textComponent}
+ {this.props.shouldShowRightIcon && (
+
+
+
+ )}
);
}
@@ -216,6 +253,7 @@ class Button extends Component {
this.props.small ? styles.buttonSmall : undefined,
this.props.medium ? styles.buttonMedium : undefined,
this.props.large ? styles.buttonLarge : undefined,
+ this.props.extraLarge ? styles.buttonExtraLarge : undefined,
this.props.success ? styles.buttonSuccess : undefined,
this.props.danger ? styles.buttonDanger : undefined,
(this.props.isDisabled && this.props.success) ? styles.buttonSuccessDisabled : undefined,
diff --git a/src/components/CheckboxWithLabel.js b/src/components/CheckboxWithLabel.js
index f494895af102..0b355d05fa21 100644
--- a/src/components/CheckboxWithLabel.js
+++ b/src/components/CheckboxWithLabel.js
@@ -10,13 +10,10 @@ import * as FormUtils from '../libs/FormUtils';
const propTypes = {
/** Whether the checkbox is checked */
- isChecked: PropTypes.bool.isRequired,
+ isChecked: PropTypes.bool,
/** Called when the checkbox or label is pressed */
- onPress: PropTypes.func.isRequired,
-
- /** Called when the checkbox or label is pressed */
- onChange: PropTypes.func,
+ onInputChange: PropTypes.func.isRequired,
/** Container styles */
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
@@ -33,6 +30,12 @@ const propTypes = {
/** Indicates that the input is being used with the Form component */
isFormInput: PropTypes.bool,
+ /** The default value for the checkbox */
+ defaultValue: PropTypes.bool,
+
+ /** React ref being forwarded to the Checkbox input */
+ forwardedRef: PropTypes.func,
+
/**
* The ID used to uniquely identify the input
*
@@ -46,7 +49,6 @@ const propTypes = {
};
const defaultProps = {
- onChange: () => {},
isFormInput: false,
inputID: undefined,
style: [],
@@ -54,16 +56,22 @@ const defaultProps = {
LabelComponent: undefined,
errorText: '',
shouldSaveDraft: false,
+ isChecked: false,
+ defaultValue: false,
+ forwardedRef: () => {},
};
-const CheckboxWithLabel = React.forwardRef((props, ref) => {
+const CheckboxWithLabel = (props) => {
const LabelComponent = props.LabelComponent;
const defaultStyles = [styles.flexRow, styles.alignItemsCenter];
const wrapperStyles = _.isArray(props.style) ? [...defaultStyles, ...props.style] : [...defaultStyles, props.style];
+ // We keep track of the checkbox "state" in a local variable so that this component has an uncontrolled input interface
+ let isChecked = props.defaultValue ? props.defaultValue : props.isChecked;
+
function toggleCheckbox() {
- props.onPress(!props.isChecked);
- props.onChange(!props.isChecked);
+ props.onInputChange(!isChecked);
+ isChecked = !isChecked;
}
if (!props.label && !LabelComponent) {
@@ -73,11 +81,11 @@ const CheckboxWithLabel = React.forwardRef((props, ref) => {
<>
{
>
);
-});
+};
CheckboxWithLabel.propTypes = propTypes;
CheckboxWithLabel.defaultProps = defaultProps;
CheckboxWithLabel.displayName = 'CheckboxWithLabel';
-export default CheckboxWithLabel;
+export default React.forwardRef((props, ref) => (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+));
diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js
index ed23d98020f2..687ac17ff91b 100644
--- a/src/components/Composer/index.android.js
+++ b/src/components/Composer/index.android.js
@@ -64,6 +64,7 @@ class Composer extends React.Component {
render() {
return (
this.textInput = el}
maxHeight={CONST.COMPOSER_MAX_HEIGHT}
diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js
index 73784ace874e..d61339f18f3f 100644
--- a/src/components/Composer/index.ios.js
+++ b/src/components/Composer/index.ios.js
@@ -76,6 +76,7 @@ class Composer extends React.Component {
const propsToPass = _.omit(this.props, 'selection');
return (
this.textInput = el}
maxHeight={CONST.COMPOSER_MAX_HEIGHT}
diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js
index caec71317e84..740d28757a6f 100755
--- a/src/components/Composer/index.js
+++ b/src/components/Composer/index.js
@@ -348,6 +348,7 @@ class Composer extends React.Component {
const propsWithoutStyles = _.omit(this.props, 'style');
return (
this.textInput = el}
selection={this.state.selection}
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index 4fe540bcc813..21ec2d724f15 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -256,6 +256,9 @@ class EmojiPickerMenu extends Component {
const isHeader = e => e.header || e.spacer;
do {
newIndex += steps;
+ if (newIndex < 0) {
+ break;
+ }
} while (isHeader(this.state.filteredEmojis[newIndex]));
};
diff --git a/src/components/Form.js b/src/components/Form.js
index ac9aaf1673fd..a648abceb0a3 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -144,7 +144,11 @@ class Form extends React.Component {
// We clone the child passing down all form props
const inputID = child.props.inputID;
const defaultValue = this.props.draftValues[inputID] || child.props.defaultValue;
- this.inputValues[inputID] = defaultValue;
+
+ // We want to initialize the input value if it's undefined
+ if (_.isUndefined(this.inputValues[inputID])) {
+ this.inputValues[inputID] = defaultValue;
+ }
return React.cloneElement(child, {
ref: node => this.inputRefs[inputID] = node,
diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js
index 9c90472b016c..8647ceecf014 100644
--- a/src/components/FormAlertWithSubmitButton.js
+++ b/src/components/FormAlertWithSubmitButton.js
@@ -26,7 +26,7 @@ const propTypes = {
buttonText: PropTypes.string.isRequired,
/** Callback fired when the "fix the errors" link is pressed */
- onFixTheErrorsLinkPressed: PropTypes.func.isRequired,
+ onFixTheErrorsLinkPressed: PropTypes.func,
/** Error message to display above button */
message: PropTypes.string,
@@ -49,6 +49,7 @@ const defaultProps = {
isMessageHtml: false,
containerStyles: [],
isLoading: false,
+ onFixTheErrorsLinkPressed: () => {},
};
const FormAlertWithSubmitButton = (props) => {
diff --git a/src/components/SignInPageForm/BaseForm.js b/src/components/FormElement.js
similarity index 63%
rename from src/components/SignInPageForm/BaseForm.js
rename to src/components/FormElement.js
index 21a4e19b1f9b..f46b24708c4c 100644
--- a/src/components/SignInPageForm/BaseForm.js
+++ b/src/components/FormElement.js
@@ -1,8 +1,8 @@
import React, {forwardRef} from 'react';
import {View} from 'react-native';
-import * as ComponentUtils from '../../libs/ComponentUtils';
+import * as ComponentUtils from '../libs/ComponentUtils';
-const BaseForm = forwardRef((props, ref) => (
+const FormElement = forwardRef((props, ref) => (
(
/>
));
-BaseForm.displayName = 'BaseForm';
-export default BaseForm;
+FormElement.displayName = 'BaseForm';
+export default FormElement;
diff --git a/src/components/FullscreenLoadingIndicator.js b/src/components/FullscreenLoadingIndicator.js
index 41a2ef2f8436..4bf55d7a3921 100644
--- a/src/components/FullscreenLoadingIndicator.js
+++ b/src/components/FullscreenLoadingIndicator.js
@@ -5,38 +5,68 @@ import {ActivityIndicator, StyleSheet, View} from 'react-native';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import stylePropTypes from '../styles/stylePropTypes';
+import Log from '../libs/Log';
+import CONST from '../CONST';
const propTypes = {
- /** Controls whether the loader is mounted and displayed */
- visible: PropTypes.bool,
+ /**
+ * Context info printed in timing log.
+ * Providing this prop would capture logs for mounting/unmounting and staying visible for too long
+ */
+ logDetail: PropTypes.shape({
+ /** Name is used to distinct the loader in captured logs. */
+ name: PropTypes.string.isRequired,
+ }),
/** Additional style props */
style: stylePropTypes,
};
const defaultProps = {
- visible: true,
style: [],
+ logDetail: null,
};
-/**
- * Loading indication component intended to cover the whole page, while the page prepares for initial render
- *
- * @param {Object} props
- * @returns {JSX.Element}
- */
-const FullScreenLoadingIndicator = (props) => {
- if (!props.visible) {
- return null;
+class FullScreenLoadingIndicator extends React.Component {
+ componentDidMount() {
+ if (!this.props.logDetail) {
+ return;
+ }
+
+ if (!this.props.logDetail.name) {
+ throw new Error('A name should be set to distinct logged messages. Please check the `logDetails` prop.');
+ }
+
+ Log.info('[LoadingIndicator] Became visible', false, this.props.logDetail);
+
+ this.timeoutID = setTimeout(
+ () => Log.alert(
+ `${CONST.ERROR.ENSURE_BUGBOT} [LoadingIndicator] Visible after timeout`,
+ {timeout: CONST.TIMING.SPINNER_TIMEOUT, ...this.props.logDetail},
+ false,
+ ),
+ CONST.TIMING.SPINNER_TIMEOUT,
+ );
}
- const additionalStyles = _.isArray(props.style) ? props.style : [props.style];
- return (
-
-
-
- );
-};
+ componentWillUnmount() {
+ if (!this.timeoutID) {
+ return;
+ }
+
+ clearTimeout(this.timeoutID);
+ Log.info('[LoadingIndicator] Disappeared', false, this.props.logDetail);
+ }
+
+ render() {
+ const additionalStyles = _.isArray(this.props.style) ? this.props.style : [this.props.style];
+ return (
+
+
+
+ );
+ }
+}
FullScreenLoadingIndicator.propTypes = propTypes;
FullScreenLoadingIndicator.defaultProps = defaultProps;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
index 6f53780a4eb4..e768e59bbf98 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
@@ -16,7 +16,7 @@ const AnchorRenderer = (props) => {
const htmlAttribs = props.tnode.attributes;
// An auth token is needed to download Expensify chat attachments
- const isAttachment = Boolean(htmlAttribs['data-expensify-source']);
+ const isAttachment = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]);
const displayName = lodashGet(props.tnode, 'domNode.children[0].data', '');
const parentStyle = lodashGet(props.tnode, 'parent.styles.nativeTextRet', {});
const attrHref = htmlAttribs.href || '';
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
index e1f9b356ab97..49e2e72829d4 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
@@ -4,7 +4,8 @@ import Config from '../../../CONFIG';
import AttachmentModal from '../../AttachmentModal';
import styles from '../../../styles/styles';
import ThumbnailImage from '../../ThumbnailImage';
-import TouchableWithoutFocus from '../../TouchableWithoutFocus';
+import PressableWithoutFocus from '../../PressableWithoutFocus';
+import CONST from '../../../CONST';
const ImageRenderer = (props) => {
const htmlAttribs = props.tnode.attributes;
@@ -14,7 +15,7 @@ const ImageRenderer = (props) => {
// - Chat Attachment images
//
// Images uploaded by the user via the app or email.
- // These have a full-sized image `htmlAttribs['data-expensify-source']`
+ // These have a full-sized image `htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]`
// and a thumbnail `htmlAttribs.src`. Both of these URLs need to have
// an authToken added to them in order to control who
// can see the images.
@@ -26,11 +27,11 @@ const ImageRenderer = (props) => {
// Concierge responder attachments are uploaded to S3 without any access
// control and thus require no authToken to verify access.
//
- const isAttachment = Boolean(htmlAttribs['data-expensify-source']);
+ const isAttachment = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]);
const originalFileName = htmlAttribs['data-name'];
let previewSource = htmlAttribs.src;
let source = isAttachment
- ? htmlAttribs['data-expensify-source']
+ ? htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]
: htmlAttribs.src;
// Update the image URL so the images can be accessed depending on the config environment
@@ -50,7 +51,7 @@ const ImageRenderer = (props) => {
originalFileName={originalFileName}
>
{({show}) => (
-
@@ -59,7 +60,7 @@ const ImageRenderer = (props) => {
style={styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachment}
/>
-
+
)}
);
diff --git a/src/components/Header.js b/src/components/Header.js
index 6fdcf113597f..afbdf1fa778a 100644
--- a/src/components/Header.js
+++ b/src/components/Header.js
@@ -29,7 +29,7 @@ const Header = props => (
{/* If there's no subtitle then display a fragment to avoid an empty space which moves the main title */}
{_.isString(props.subtitle)
- ? Boolean(props.subtitle) && {props.subtitle}
+ ? Boolean(props.subtitle) && {props.subtitle}
: props.subtitle}
{props.shouldShowEnvironmentBadge && (
diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js
index 4dc6b42dff85..4d98f1146930 100644
--- a/src/components/Icon/Expensicons.js
+++ b/src/components/Icon/Expensicons.js
@@ -74,6 +74,7 @@ import ActiveRoomAvatar from '../../../assets/images/avatars/room.svg';
import DeletedRoomAvatar from '../../../assets/images/avatars/deleted-room.svg';
import AdminRoomAvatar from '../../../assets/images/avatars/admin-room.svg';
import AnnounceRoomAvatar from '../../../assets/images/avatars/announce-room.svg';
+import Connect from '../../../assets/images/connect.svg';
export {
ActiveRoomAvatar,
@@ -98,6 +99,7 @@ export {
Close,
ClosedSign,
Concierge,
+ Connect,
CreditCard,
DeletedRoomAvatar,
DownArrow,
diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js
index 30eba46aba9e..aafb06ea6fd2 100644
--- a/src/components/LocalePicker.js
+++ b/src/components/LocalePicker.js
@@ -49,7 +49,7 @@ const LocalePicker = (props) => {
return (
{
+ onInputChange={(locale) => {
if (locale === props.preferredLocale) {
return;
}
diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js
index 993d36123c3a..0bd2bb5da688 100644
--- a/src/components/MultipleAvatars.js
+++ b/src/components/MultipleAvatars.js
@@ -12,7 +12,7 @@ import CONST from '../CONST';
const propTypes = {
/** Array of avatar URLs or icons */
- avatarIcons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])),
+ icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])),
/** Set the size of avatars */
size: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)),
@@ -26,7 +26,7 @@ const propTypes = {
};
const defaultProps = {
- avatarIcons: [],
+ icons: [],
size: CONST.AVATAR_SIZE.DEFAULT,
secondAvatarStyle: [StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)],
avatarTooltips: [],
@@ -40,16 +40,16 @@ const MultipleAvatars = (props) => {
...props.secondAvatarStyle,
];
- if (!props.avatarIcons.length) {
+ if (!props.icons.length) {
return null;
}
- if (props.avatarIcons.length === 1) {
+ if (props.icons.length === 1) {
return (
@@ -65,17 +65,17 @@ const MultipleAvatars = (props) => {
>
- {props.avatarIcons.length === 2 ? (
+ {props.icons.length === 2 ? (
@@ -88,7 +88,7 @@ const MultipleAvatars = (props) => {
? styles.avatarInnerTextSmall
: styles.avatarInnerText}
>
- {`+${props.avatarIcons.length - 1}`}
+ {`+${props.icons.length - 1}`}
diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js
index 535209aec91b..9d8a4119eb4c 100644
--- a/src/components/OptionRow.js
+++ b/src/components/OptionRow.js
@@ -173,7 +173,7 @@ const OptionRow = (props) => {
/>
) : (
(
@@ -238,21 +247,25 @@ class OptionsSelector extends Component {
selectTextOnFocus
/>
- this.list = el}
- optionHoveredStyle={styles.hoveredComponentBG}
- onSelectRow={this.selectRow}
- sections={this.props.sections}
- focusedIndex={this.state.focusedIndex}
- selectedOptions={this.props.selectedOptions}
- canSelectMultipleOptions={this.props.canSelectMultipleOptions}
- hideSectionHeaders={this.props.hideSectionHeaders}
- headerMessage={this.props.headerMessage}
- disableFocusOptions={this.props.disableArrowKeysActions}
- hideAdditionalOptionStates={this.props.hideAdditionalOptionStates}
- forceTextUnreadStyle={this.props.forceTextUnreadStyle}
- showTitleTooltip={this.props.showTitleTooltip}
- />
+ {this.props.shouldShowOptions
+ ? (
+ this.list = el}
+ optionHoveredStyle={styles.hoveredComponentBG}
+ onSelectRow={this.selectRow}
+ sections={this.props.sections}
+ focusedIndex={this.state.focusedIndex}
+ selectedOptions={this.props.selectedOptions}
+ canSelectMultipleOptions={this.props.canSelectMultipleOptions}
+ hideSectionHeaders={this.props.hideSectionHeaders}
+ headerMessage={this.props.headerMessage}
+ disableFocusOptions={this.props.disableArrowKeysActions}
+ hideAdditionalOptionStates={this.props.hideAdditionalOptionStates}
+ forceTextUnreadStyle={this.props.forceTextUnreadStyle}
+ showTitleTooltip={this.props.showTitleTooltip}
+ />
+ )
+ : }
);
}
diff --git a/src/components/Picker/BasePicker/basePickerPropTypes.js b/src/components/Picker/BasePicker/basePickerPropTypes.js
index 02f5ff2d2b6d..7af66b59bdf6 100644
--- a/src/components/Picker/BasePicker/basePickerPropTypes.js
+++ b/src/components/Picker/BasePicker/basePickerPropTypes.js
@@ -6,7 +6,7 @@ import * as Expensicons from '../../Icon/Expensicons';
const propTypes = {
/** A callback method that is called when the value changes and it received the selected value as an argument */
- onChange: PropTypes.func.isRequired,
+ onInputChange: PropTypes.func.isRequired,
/** Whether or not to show the disabled styles */
disabled: PropTypes.bool,
diff --git a/src/components/Picker/BasePicker/basePickerStyles/index.js b/src/components/Picker/BasePicker/basePickerStyles/index.js
index d0abe76f952b..8531ccbe3bb1 100644
--- a/src/components/Picker/BasePicker/basePickerStyles/index.js
+++ b/src/components/Picker/BasePicker/basePickerStyles/index.js
@@ -1,9 +1,9 @@
import CONST from '../../../../CONST';
-import getBrowser from '../../../../libs/getBrowser';
+import * as Browser from '../../../../libs/Browser';
import styles from '../../../../styles/styles';
const pickerStylesWeb = () => {
- if (CONST.BROWSER.FIREFOX === getBrowser()) {
+ if (CONST.BROWSER.FIREFOX === Browser.getBrowser()) {
return {
textIndent: -2,
};
diff --git a/src/components/Picker/BasePicker/index.js b/src/components/Picker/BasePicker/index.js
index 75d06cfbc84c..07af4b36d0b1 100644
--- a/src/components/Picker/BasePicker/index.js
+++ b/src/components/Picker/BasePicker/index.js
@@ -1,32 +1,53 @@
import React from 'react';
import RNPickerSelect from 'react-native-picker-select';
+import _ from 'underscore';
import styles from '../../../styles/styles';
import * as basePickerPropTypes from './basePickerPropTypes';
import basePickerStyles from './basePickerStyles';
-const BasePicker = props => (
- props.icon(props.size)}
- disabled={props.disabled}
- fixAndroidTouchableBug
- onOpen={props.onOpen}
- onClose={props.onClose}
- pickerProps={{
- onFocus: props.onOpen,
- onBlur: props.onClose,
- }}
- />
-);
+class BasePicker extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ selectedValue: this.props.defaultValue,
+ };
+
+ this.updateSelectedValueAndExecuteOnChange = this.updateSelectedValueAndExecuteOnChange.bind(this);
+ }
+
+ updateSelectedValueAndExecuteOnChange(value) {
+ this.props.onInputChange(value);
+ this.setState({selectedValue: value});
+ }
+
+ render() {
+ const hasError = !_.isEmpty(this.props.errorText);
+ return (
+ this.props.icon(this.props.size)}
+ disabled={this.props.disabled}
+ fixAndroidTouchableBug
+ onOpen={this.props.onOpen}
+ onClose={this.props.onClose}
+ pickerProps={{
+ onFocus: this.props.onOpen,
+ onBlur: this.props.onBlur,
+ ref: this.props.innerRef,
+ }}
+ />
+ );
+ }
+}
BasePicker.propTypes = basePickerPropTypes.propTypes;
BasePicker.defaultProps = basePickerPropTypes.defaultProps;
-BasePicker.displayName = 'BasePicker';
export default BasePicker;
diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js
index b7277fcda73e..5f2f86374854 100644
--- a/src/components/Picker/index.js
+++ b/src/components/Picker/index.js
@@ -6,6 +6,7 @@ import BasePicker from './BasePicker';
import Text from '../Text';
import styles from '../../styles/styles';
import InlineErrorText from '../InlineErrorText';
+import * as FormUtils from '../../libs/FormUtils';
const propTypes = {
/** Picker label */
@@ -14,22 +15,39 @@ const propTypes = {
/** Should the picker appear disabled? */
isDisabled: PropTypes.bool,
- /** Should the input be styled for errors */
- hasError: PropTypes.bool,
+ /** Input value */
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Error text to display */
errorText: PropTypes.string,
/** Customize the Picker container */
containerStyles: PropTypes.arrayOf(PropTypes.object),
+
+ /** Indicates that the input is being used with the Form component */
+ isFormInput: PropTypes.bool,
+
+ /**
+ * The ID used to uniquely identify the input
+ *
+ * @param {Object} props - props passed to the input
+ * @returns {Object} - returns an Error object if isFormInput is supplied but inputID is falsey or not a string
+ */
+ inputID: props => FormUtils.validateInputIDProps(props),
+
+ /** Saves a draft of the input value when used in a form */
+ shouldSaveDraft: PropTypes.bool,
};
const defaultProps = {
label: '',
isDisabled: false,
- hasError: false,
errorText: '',
containerStyles: [],
+ isFormInput: false,
+ inputID: undefined,
+ shouldSaveDraft: false,
+ value: undefined,
};
class Picker extends PureComponent {
@@ -48,6 +66,7 @@ class Picker extends PureComponent {
style={[
styles.pickerContainer,
this.props.isDisabled && styles.inputDisabled,
+ ...this.props.containerStyles,
]}
>
{this.props.label && (
@@ -58,12 +77,13 @@ class Picker extends PureComponent {
onClose={() => this.setState({isOpen: false})}
disabled={this.props.isDisabled}
focused={this.state.isOpen}
- hasError={this.props.hasError}
+ errorText={this.props.errorText}
+ value={this.props.value}
// eslint-disable-next-line react/jsx-props-no-spreading
{...pickerProps}
/>
-
+
{this.props.errorText}
>
@@ -74,4 +94,5 @@ class Picker extends PureComponent {
Picker.propTypes = propTypes;
Picker.defaultProps = defaultProps;
-export default Picker;
+// eslint-disable-next-line react/jsx-props-no-spreading
+export default React.forwardRef((props, ref) => );
diff --git a/src/components/TouchableWithoutFocus.js b/src/components/PressableWithoutFocus.js
similarity index 54%
rename from src/components/TouchableWithoutFocus.js
rename to src/components/PressableWithoutFocus.js
index dddeb4c2cd33..35a9cce995ed 100644
--- a/src/components/TouchableWithoutFocus.js
+++ b/src/components/PressableWithoutFocus.js
@@ -1,5 +1,5 @@
import React from 'react';
-import {TouchableOpacity} from 'react-native';
+import {Pressable} from 'react-native';
import PropTypes from 'prop-types';
const propTypes = {
@@ -21,29 +21,33 @@ const defaultProps = {
};
/**
- * This component prevents the tapped element from capturing focus
+ * This component prevents the tapped element from capturing focus.
+ * We need to blur this element when clicked as it opens modal that implements focus-trapping.
+ * When the modal is closed it focuses back to the last active element.
+ * Therefore it shifts the element to bring it back to focus.
+ * https://github.com/Expensify/App/issues/6806
*/
-class TouchableWithoutFocus extends React.Component {
+class PressableWithoutFocus extends React.Component {
constructor(props) {
super(props);
this.pressAndBlur = this.pressAndBlur.bind(this);
}
pressAndBlur() {
- this.touchableRef.blur();
+ this.pressableRef.blur();
this.props.onPress();
}
render() {
return (
- this.touchableRef = el} style={this.props.styles}>
+ this.pressableRef = el} style={this.props.styles}>
{this.props.children}
-
+
);
}
}
-TouchableWithoutFocus.propTypes = propTypes;
-TouchableWithoutFocus.defaultProps = defaultProps;
+PressableWithoutFocus.propTypes = propTypes;
+PressableWithoutFocus.defaultProps = defaultProps;
-export default TouchableWithoutFocus;
+export default PressableWithoutFocus;
diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/IOUPreview.js
index 309a46bff735..60c49047be04 100644
--- a/src/components/ReportActionItem/IOUPreview.js
+++ b/src/components/ReportActionItem/IOUPreview.js
@@ -139,7 +139,7 @@ const IOUPreview = (props) => {
diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js
index d60c8bd6c745..0d86d1071018 100644
--- a/src/components/ReportWelcomeText.js
+++ b/src/components/ReportWelcomeText.js
@@ -10,7 +10,6 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize';
import compose from '../libs/compose';
import * as ReportUtils from '../libs/reportUtils';
import * as OptionsListUtils from '../libs/OptionsListUtils';
-import * as Localize from '../libs/Localize';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
@@ -91,7 +90,7 @@ const ReportWelcomeText = (props) => {
{props.translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo')}
- {lodashGet(props.policies, [`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`, 'name'], Localize.translateLocal('workspace.common.unavailable'))}
+ {ReportUtils.getPolicyName(props.report, props.policies)}
{props.translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree')}
diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js
index f9cf7351ae32..e70ed68a9fc4 100644
--- a/src/components/RoomHeaderAvatars.js
+++ b/src/components/RoomHeaderAvatars.js
@@ -11,26 +11,26 @@ import * as StyleUtils from '../styles/StyleUtils';
const propTypes = {
/** Array of avatar URLs or icons */
- avatarIcons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])),
+ icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])),
/** Whether show large Avatars */
shouldShowLargeAvatars: PropTypes.bool,
};
const defaultProps = {
- avatarIcons: [],
+ icons: [],
shouldShowLargeAvatars: false,
};
const RoomHeaderAvatars = (props) => {
- if (!props.avatarIcons.length) {
+ if (!props.icons.length) {
return null;
}
- if (props.avatarIcons.length === 1) {
+ if (props.icons.length === 1) {
return (
{
{
@@ -60,15 +60,15 @@ const RoomHeaderAvatars = (props) => {
);
}
- const avatarIconsToDisplay = props.avatarIcons.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS);
+ const iconsToDisplay = props.icons.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS);
return (
- {_.map(avatarIconsToDisplay, (val, index) => (
+ {_.map(iconsToDisplay, (val, index) => (
- {index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.avatarIcons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && (
+ {index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && (
<>
{
]}
/>
- {`+${props.avatarIcons.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`}
+ {`+${props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`}
>
)}
diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js
index acbdfcdc36d6..178ad3d24b54 100644
--- a/src/components/RoomNameInput.js
+++ b/src/components/RoomNameInput.js
@@ -56,29 +56,17 @@ const defaultProps = {
class RoomNameInput extends Component {
constructor(props) {
super(props);
- this.state = {
- roomName: props.initialValue,
- };
this.setModifiedRoomName = this.setModifiedRoomName.bind(this);
}
/**
- * Sets the modified room name in the state and calls the onChangeText callback
+ * Calls the onChangeText callback with a modified room name
* @param {Event} event
*/
setModifiedRoomName(event) {
- const nativeEvent = event.nativeEvent;
- const roomName = nativeEvent.text;
- const target = nativeEvent.target;
- const selection = target.selectionStart;
+ const roomName = event.nativeEvent.text;
const modifiedRoomName = this.modifyRoomName(roomName);
- this.setState({roomName: modifiedRoomName}, () => {
- if (!selection) {
- return;
- }
- target.selectionEnd = selection;
- });
this.props.onChangeText(modifiedRoomName);
}
@@ -108,7 +96,7 @@ class RoomNameInput extends Component {
prefixCharacter={CONST.POLICY.ROOM_PREFIX}
placeholder={this.props.translate('newRoomPage.social')}
onChange={this.setModifiedRoomName}
- value={this.state.roomName.substring(1)}
+ defaultValue={this.props.initialValue.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice.
errorText={this.props.errorText}
autoCapitalize="none"
/>
diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js
index 0c434934ca9d..2997d2cadc60 100644
--- a/src/components/SettlementButton.js
+++ b/src/components/SettlementButton.js
@@ -15,6 +15,9 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize';
import KYCWall from './KYCWall';
const propTypes = {
+ /** Callback to execute when this button is pressed. Receives a single payment type argument. */
+ onPress: PropTypes.func.isRequired,
+
/** Settlement currency type */
currency: PropTypes.string,
diff --git a/src/components/SignInPageForm/index.js b/src/components/SignInPageForm/index.js
index 163f1ee223b1..4ee95d1cb81e 100644
--- a/src/components/SignInPageForm/index.js
+++ b/src/components/SignInPageForm/index.js
@@ -1,5 +1,5 @@
import React from 'react';
-import BaseForm from './BaseForm';
+import FormElement from '../FormElement';
class Form extends React.Component {
componentDidMount() {
@@ -16,7 +16,7 @@ class Form extends React.Component {
render() {
return (
- this.form = el}
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.props}
diff --git a/src/components/SignInPageForm/index.native.js b/src/components/SignInPageForm/index.native.js
index 21f10e7a428d..d09e60c1b98d 100644
--- a/src/components/SignInPageForm/index.native.js
+++ b/src/components/SignInPageForm/index.native.js
@@ -1,8 +1,8 @@
import React from 'react';
-import BaseForm from './BaseForm';
+import FormElement from '../FormElement';
// eslint-disable-next-line react/jsx-props-no-spreading
-const Form = props => ;
+const Form = props => ;
Form.displayName = 'Form';
export default Form;
diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js
index d9fbe810eeb4..11d4cad09f55 100644
--- a/src/components/StatePicker.js
+++ b/src/components/StatePicker.js
@@ -32,7 +32,7 @@ const StatePicker = props => (
(
+ <>
+
+ Test Preferences
+
+
+ {/* Option to switch from using the staging secure endpoint or the production secure endpoint.
+ This enables QA and internal testers to take advantage of sandbox environments for 3rd party services like Plaid and Onfido. */}
+
+ User.setShouldUseSecureStaging(!props.user.shouldUseSecureStaging)}
+ />
+
+
+ {/* When toggled all network requests will fail. */}
+
+ Network.setShouldFailAllRequests(!props.network.shouldFailAllRequests)}
+ />
+
+
+ {/* Instantly invalidates a user's local authToken. Useful for testing flows related to reauthentication. */}
+
+
+
+ {/* Invalidate stored user auto-generated credentials. Useful for manually testing sign out logic. */}
+
+
+ >
+);
+
+TestToolMenu.propTypes = propTypes;
+TestToolMenu.defaultProps = defaultProps;
+export default withOnyx({
+ user: {
+ key: ONYXKEYS.USER,
+ },
+ network: {
+ key: ONYXKEYS.NETWORK,
+ },
+})(TestToolMenu);
diff --git a/src/components/TestToolRow.js b/src/components/TestToolRow.js
new file mode 100644
index 000000000000..ac7afd0c6da2
--- /dev/null
+++ b/src/components/TestToolRow.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {View} from 'react-native';
+import styles from '../styles/styles';
+import Text from './Text';
+
+const propTypes = {
+ /** Title of control */
+ title: PropTypes.string.isRequired,
+
+ /** Control component jsx */
+ children: PropTypes.node.isRequired,
+};
+
+const TestToolRow = props => (
+
+
+
+ {props.title}
+
+
+
+ {props.children}
+
+
+);
+
+TestToolRow.propTypes = propTypes;
+export default TestToolRow;
diff --git a/src/components/WalletStatementModal/index.js b/src/components/WalletStatementModal/index.js
index 655ef2421e29..60d57fc5c14b 100644
--- a/src/components/WalletStatementModal/index.js
+++ b/src/components/WalletStatementModal/index.js
@@ -41,9 +41,7 @@ class WalletStatementModal extends React.Component {
const authToken = lodashGet(this.props, 'session.authToken', null);
return (
<>
-
+ {this.state.isLoading && }
diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js
index b369a12e59a8..d0d30095d285 100644
--- a/src/pages/ReportDetailsPage.js
+++ b/src/pages/ReportDetailsPage.js
@@ -133,7 +133,7 @@ class ReportDetailsPage extends Component {
>
diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js
index 6ee169d0256d..669b47b2d1c3 100644
--- a/src/pages/ReportSettingsPage.js
+++ b/src/pages/ReportSettingsPage.js
@@ -168,7 +168,7 @@ class ReportSettingsPage extends Component {
{
+ onInputChange={(notificationPreference) => {
Report.updateNotificationPreference(
this.props.report.reportID,
notificationPreference,
@@ -234,7 +234,9 @@ class ReportSettingsPage extends Component {
{this.props.translate('newRoomPage.visibility')}
- {this.props.report.visibility}
+
+ {this.props.translate(`newRoomPage.visibilityOptions.${this.props.report.visibility}`)}
+
{
this.props.report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED
diff --git a/src/pages/RequestCallPage.js b/src/pages/RequestCallPage.js
index 84504769a867..582c056b87b9 100644
--- a/src/pages/RequestCallPage.js
+++ b/src/pages/RequestCallPage.js
@@ -31,6 +31,7 @@ import * as LoginUtils from '../libs/LoginUtils';
import * as ValidationUtils from '../libs/ValidationUtils';
import * as PersonalDetails from '../libs/actions/PersonalDetails';
import * as User from '../libs/actions/User';
+import FormElement from '../components/FormElement';
const propTypes = {
...withLocalizePropTypes,
@@ -262,45 +263,47 @@ class RequestCallPage extends Component {
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
-
-
- {this.props.translate('requestCallPage.description')}
-
- this.setState({firstName})}
- onChangeLastName={lastName => this.setState({lastName})}
- style={[styles.mv4]}
- />
- this.setState({phoneNumber})}
- />
- this.setState({phoneExtension})}
- containerStyles={[styles.mt4]}
- />
- {this.getWaitTimeMessage()}
-
+
+
+
+ {this.props.translate('requestCallPage.description')}
+
+ this.setState({firstName})}
+ onChangeLastName={lastName => this.setState({lastName})}
+ style={[styles.mv4]}
+ />
+ this.setState({phoneNumber})}
+ />
+ this.setState({phoneExtension})}
+ containerStyles={[styles.mt4]}
+ />
+ {this.getWaitTimeMessage()}
+
+
{isBlockedFromConcierge && (
diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js
index 1fc76da9c3be..30212fc2c711 100755
--- a/src/pages/SearchPage.js
+++ b/src/pages/SearchPage.js
@@ -16,7 +16,6 @@ import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
import ScreenWrapper from '../components/ScreenWrapper';
import Timing from '../libs/actions/Timing';
import CONST from '../CONST';
-import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';
import personalDetailsPropType from './personalDetailsPropType';
@@ -90,18 +89,22 @@ class SearchPage extends Component {
* @returns {Array}
*/
getSections() {
- const sections = [
- {
+ const sections = [];
+ if (this.state.recentReports.length > 0) {
+ sections.push(({
data: this.state.recentReports,
shouldShow: true,
indexOffset: 0,
- },
- {
+ }));
+ }
+
+ if (this.state.personalDetails.length > 0) {
+ sections.push(({
data: this.state.personalDetails,
shouldShow: true,
indexOffset: this.state.recentReports.length,
- },
- ];
+ }));
+ }
if (this.state.userToInvite) {
sections.push(({
@@ -173,19 +176,17 @@ class SearchPage extends Component {
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
-
- {didScreenTransitionEnd && (
-
- )}
+
>
diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js
index 4fd9f41ae999..2b6a36a9c6f5 100755
--- a/src/pages/SetPasswordPage.js
+++ b/src/pages/SetPasswordPage.js
@@ -14,12 +14,11 @@ import {
import styles from '../styles/styles';
import * as Session from '../libs/actions/Session';
import ONYXKEYS from '../ONYXKEYS';
-import Button from '../components/Button';
import SignInPageLayout from './signin/SignInPageLayout';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';
import NewPasswordForm from './settings/NewPasswordForm';
-import Text from '../components/Text';
+import FormAlertWithSubmitButton from '../components/FormAlertWithSubmitButton';
const propTypes = {
/* Onyx Props */
@@ -104,7 +103,12 @@ class SetPasswordPage extends Component {
}
const accountID = lodashGet(this.props.route.params, 'accountID', '');
const validateCode = lodashGet(this.props.route.params, 'validateCode', '');
- Session.setOrChangePassword(accountID, validateCode, this.state.password, this.props.userSignUp.authToken);
+
+ if (this.props.userSignUp.authToken) {
+ Session.changePasswordAndSignIn(this.props.userSignUp.authToken, this.state.password);
+ } else {
+ Session.setPassword(this.state.password, validateCode, accountID);
+ }
}
render() {
@@ -117,29 +121,24 @@ class SetPasswordPage extends Component {
shouldShowWelcomeText
welcomeText={this.props.translate('setPasswordPage.passwordFormTitle')}
>
- {_.isEmpty(error) ? (
- <>
-
- this.setState({password})}
- updateIsFormValid={isValid => this.setState({isFormValid: isValid})}
- onSubmitEditing={this.validateAndSubmitForm}
- />
-
-
-
-
- >
- )
- : ({error})}
+
+ this.setState({password})}
+ updateIsFormValid={isValid => this.setState({isFormValid: isValid})}
+ onSubmitEditing={this.validateAndSubmitForm}
+ />
+
+
+
+
);
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index dd7c58ac2440..b01adba833b4 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -100,8 +100,8 @@ const HeaderView = (props) => {
// these users via alternative means. It is possible to request a call with Concierge so we leave the option for them.
const shouldShowCallButton = isConcierge || !isAutomatedExpensifyAccount;
const avatarTooltip = isChatRoom ? undefined : _.pluck(displayNamesWithTooltips, 'tooltip');
- const shouldShowSubscript = isPolicyExpenseChat && !props.report.isOwnPolicyExpenseChat;
- const avatarIcons = OptionsListUtils.getAvatarSources(props.report);
+ const shouldShowSubscript = isPolicyExpenseChat && !props.report.isOwnPolicyExpenseChat && !ReportUtils.isArchivedRoom(props.report);
+ const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies);
return (
@@ -138,14 +138,14 @@ const HeaderView = (props) => {
>
{shouldShowSubscript ? (
) : (
)}
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index ae4acbdd4820..a90ad90a9570 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -183,7 +183,7 @@ class ReportScreen extends React.Component {
nativeID={CONST.REPORT.DROP_NATIVE_ID}
style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]}
>
-
+ {this.shouldShowLoader() && }
{!this.shouldShowLoader() && (
{
- const message = _.last(lodashGet(reportAction, 'message', null));
+ const message = _.last(lodashGet(reportAction, 'message', [{}]));
const html = lodashGet(message, 'html', '');
const parser = new ExpensiMark();
@@ -72,7 +72,7 @@ export default [
const isAttachment = _.has(reportAction, 'isAttachment')
? reportAction.isAttachment
- : ReportUtils.isReportMessageAttachment(text);
+ : ReportUtils.isReportMessageAttachment(message);
if (!isAttachment) {
Clipboard.setString(text);
} else {
@@ -97,7 +97,7 @@ export default [
successIcon: Expensicons.Checkmark,
shouldShow: type => type === CONTEXT_MENU_TYPES.REPORT_ACTION,
onPress: (closePopover, {reportAction, reportID}) => {
- Report.updateLastReadActionID(reportID, reportAction.sequenceNumber);
+ Report.updateLastReadActionID(reportID, reportAction.sequenceNumber, true);
Report.setNewMarkerPosition(reportID, reportAction.sequenceNumber);
if (closePopover) {
hideContextMenu(true, ReportActionComposeFocusManager.focus);
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index 6517fef5d24a..94baafa5bd59 100755
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -370,8 +370,8 @@ class ReportActionCompose extends React.Component {
const trimmedComment = this.comment.trim();
- // Don't submit empty comments
- if (!trimmedComment) {
+ // Don't submit empty comments or comments that exceed the character limit
+ if (!trimmedComment || trimmedComment.length > CONST.MAX_COMMENT_LENGTH) {
return;
}
@@ -401,6 +401,8 @@ class ReportActionCompose extends React.Component {
const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth;
const isBlockedFromConcierge = ReportUtils.chatIncludesConcierge(this.props.report) && User.isBlockedFromConcierge(this.props.blockedFromConcierge);
const inputPlaceholder = this.getInputPlaceholder();
+ const hasExceededMaxCommentLength = this.comment.length > CONST.MAX_COMMENT_LENGTH;
+
return (
{shouldShowReportRecipientLocalTime
@@ -411,6 +413,7 @@ class ReportActionCompose extends React.Component {
: styles.chatItemComposeBoxColor,
styles.chatItemComposeBox,
styles.flexRow,
+ hasExceededMaxCommentLength && styles.borderColorDanger,
]}
>
e.preventDefault()}
- disabled={this.state.isCommentEmpty || isBlockedFromConcierge}
+ disabled={this.state.isCommentEmpty || isBlockedFromConcierge || hasExceededMaxCommentLength}
hitSlop={{
top: 3, right: 3, bottom: 3, left: 3,
}}
@@ -583,24 +586,31 @@ class ReportActionCompose extends React.Component {
- {this.props.network.isOffline ? (
-
-
-
-
- {this.props.translate('reportActionCompose.youAppearToBeOffline')}
-
-
+
+
+ {this.props.network.isOffline ? (
+
+
+
+ {this.props.translate('reportActionCompose.youAppearToBeOffline')}
+
+
+ ) : }
- ) : }
+ {hasExceededMaxCommentLength && (
+
+ {`${this.comment.length}/${CONST.MAX_COMMENT_LENGTH}`}
+
+ )}
+
);
}
diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js
index 670539b9d026..984fc13e1f50 100644
--- a/src/pages/home/report/ReportActionItemCreated.js
+++ b/src/pages/home/report/ReportActionItemCreated.js
@@ -5,9 +5,9 @@ import PropTypes from 'prop-types';
import ONYXKEYS from '../../../ONYXKEYS';
import RoomHeaderAvatars from '../../../components/RoomHeaderAvatars';
import ReportWelcomeText from '../../../components/ReportWelcomeText';
+import participantPropTypes from '../../../components/participantPropTypes';
import * as ReportUtils from '../../../libs/reportUtils';
import styles from '../../../styles/styles';
-import * as OptionsListUtils from '../../../libs/OptionsListUtils';
const propTypes = {
/** The report currently being looked at */
@@ -17,15 +17,27 @@ const propTypes = {
/** Whether the user is not an admin of policyExpenseChat chat */
isOwnPolicyExpenseChat: PropTypes.bool,
+
+ }),
+
+ /** Personal details of all the users */
+ personalDetails: PropTypes.objectOf(participantPropTypes),
+
+ /** The policies which the user has access to and which the report could be tied to */
+ policies: PropTypes.shape({
+ /** Name of the policy */
+ name: PropTypes.string,
}),
};
const defaultProps = {
report: {},
+ personalDetails: {},
+ policies: {},
};
const ReportActionItemCreated = (props) => {
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report);
- const avatarIcons = OptionsListUtils.getAvatarSources(props.report);
+ const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policies);
return (
{
>
@@ -52,4 +64,10 @@ export default withOnyx({
report: {
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
},
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS,
+ },
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
})(ReportActionItemCreated);
diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js
index 4e9ea6715af5..194a865636f7 100644
--- a/src/pages/home/report/ReportActionItemFragment.js
+++ b/src/pages/home/report/ReportActionItemFragment.js
@@ -114,9 +114,7 @@ const ReportActionItemFragment = (props) => {
fontSize={variables.fontSizeSmall}
color={themeColors.textSupporting}
>
- {/* Native devices do not support margin between nested Text */}
- {' '}
- {props.translate('reportActionCompose.edited')}
+ {` ${props.translate('reportActionCompose.edited')}`}
)}
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index 82d98997e2c3..bc3c0ddcab38 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -16,6 +16,7 @@ import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
import {withPersonalDetails} from '../../../components/OnyxProvider';
import Tooltip from '../../../components/Tooltip';
+import ControlSelection from '../../../libs/ControlSelection';
const propTypes = {
/** All the data of the action */
@@ -63,7 +64,12 @@ const ReportActionItemSingle = (props) => {
return (
- showUserDetails(props.action.actorEmail)}>
+ showUserDetails(props.action.actorEmail)}
+ >
{
{props.showHeader ? (
- showUserDetails(props.action.actorEmail)}>
+ showUserDetails(props.action.actorEmail)}
+ >
{_.map(personArray, (fragment, index) => (
{
- this.copySelectionToClipboard();
- }, copyShortcutConfig.descriptionKey, copyShortcutConfig.modifiers, false);
+ this.unsubscribeCopyShortcut = KeyboardShortcut.subscribe(
+ copyShortcutConfig.shortcutKey,
+ this.copySelectionToClipboard,
+ copyShortcutConfig.descriptionKey,
+ copyShortcutConfig.modifiers,
+ false,
+ );
}
shouldComponentUpdate(nextProps, nextState) {
@@ -276,7 +280,7 @@ class ReportActionsView extends React.Component {
Report.updateLastReadActionID(this.props.reportID);
}
- copySelectionToClipboard = () => {
+ copySelectionToClipboard() {
const selectionMarkdown = SelectionScraper.getAsMarkdown();
Clipboard.setString(selectionMarkdown);
@@ -470,8 +474,9 @@ class ReportActionsView extends React.Component {
* Hide the new MarkerBadge
*/
hideMarker() {
- this.setState({isMarkerActive: false}, () => {
- this.setState({localUnreadActionCount: 0});
+ this.setState({
+ isMarkerActive: false,
+ localUnreadActionCount: 0,
});
}
diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js
index aea493cadeb3..ee43a3a004ff 100755
--- a/src/pages/home/report/ReportTypingIndicator.js
+++ b/src/pages/home/report/ReportTypingIndicator.js
@@ -1,5 +1,4 @@
import React from 'react';
-import {View} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
@@ -50,7 +49,7 @@ class ReportTypingIndicator extends React.Component {
// Decide on the Text element that will hold the display based on the number of users that are typing.
switch (numUsersTyping) {
case 0:
- return ;
+ return <>>;
case 1:
return (
-
- {this.props.translate('reportTypingIndicator.multipleUsers')}
- {` ${this.props.translate('reportTypingIndicator.areTyping')}`}
-
-
+
+ {this.props.translate('reportTypingIndicator.multipleUsers')}
+ {` ${this.props.translate('reportTypingIndicator.areTyping')}`}
+
);
}
}
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 554dc7c1a2d3..3a77109ca9ad 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -24,6 +24,7 @@ import participantPropTypes from '../../../components/participantPropTypes';
import themeColors from '../../../styles/themes/default';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import * as App from '../../../libs/actions/App';
+import * as ReportUtils from '../../../libs/reportUtils';
const propTypes = {
/** Toggles the navigation menu open and closed */
@@ -43,6 +44,9 @@ const propTypes = {
unreadActionCount: PropTypes.number,
})),
+ /** Reports having a draft */
+ reportsWithDraft: PropTypes.objectOf(PropTypes.bool),
+
/** List of users' personal details */
personalDetails: PropTypes.objectOf(participantPropTypes),
@@ -81,9 +85,10 @@ const propTypes = {
const defaultProps = {
reports: {},
+ reportsWithDraft: {},
personalDetails: {},
myPersonalDetails: {
- avatar: OptionsListUtils.getDefaultAvatar(),
+ avatar: ReportUtils.getDefaultAvatar(),
},
network: null,
currentlyViewedReportID: '',
@@ -114,7 +119,17 @@ class SidebarLinks extends React.Component {
return unreadReports;
}
- static shouldReorder(nextProps, orderedReports, currentlyViewedReportID, unreadReports) {
+ /**
+ * Returns true if the sidebar list should be re-ordered
+ *
+ * @param {Object} nextProps
+ * @param {Boolean} hasActiveDraftHistory
+ * @param {Array} orderedReports
+ * @param {String} currentlyViewedReportID
+ * @param {Array} unreadReports
+ * @returns {Boolean}
+ */
+ static shouldReorder(nextProps, hasActiveDraftHistory, orderedReports, currentlyViewedReportID, unreadReports) {
// We do not want to re-order reports in the LHN if the only change is the draft comment in the
// current report.
@@ -144,10 +159,9 @@ class SidebarLinks extends React.Component {
return true;
}
- // Do not re-order if the active report has a draft
- if (nextProps.currentlyViewedReportID) {
- const hasActiveReportDraft = lodashGet(nextProps.reportsWithDraft, `${ONYXKEYS.COLLECTION.REPORTS_WITH_DRAFT}${nextProps.currentlyViewedReportID}`, false);
- return !hasActiveReportDraft;
+ // If there is an active report that either had or has a draft, we do not want to re-order the list
+ if (nextProps.currentlyViewedReportID && hasActiveDraftHistory) {
+ return false;
}
return true;
@@ -156,7 +170,11 @@ class SidebarLinks extends React.Component {
constructor(props) {
super(props);
this.state = {
- currentlyViewedReportID: props.currentlyViewedReportID,
+ activeReport: {
+ reportID: props.currentlyViewedReportID,
+ hasDraftHistory: lodashGet(props.reportsWithDraft, `${ONYXKEYS.COLLECTION.REPORTS_WITH_DRAFT}${props.currentlyViewedReportID}`, false),
+ lastMessageTimestamp: lodashGet(props.reports, `${ONYXKEYS.COLLECTION.REPORT}${props.currentlyViewedReportID}.lastMessageTimestamp`, 0),
+ },
orderedReports: [],
priorityMode: props.priorityMode,
unreadReports: SidebarLinks.getUnreadReports(props.reports || {}),
@@ -164,7 +182,24 @@ class SidebarLinks extends React.Component {
}
static getDerivedStateFromProps(nextProps, prevState) {
- const shouldReorder = SidebarLinks.shouldReorder(nextProps, prevState.orderedReports, prevState.currentlyViewedReportID, prevState.unreadReports);
+ const isActiveReportSame = prevState.activeReport.reportID === nextProps.currentlyViewedReportID;
+ const lastMessageTimestamp = lodashGet(nextProps.reports, `${ONYXKEYS.COLLECTION.REPORT}${nextProps.currentlyViewedReportID}.lastMessageTimestamp`, 0);
+
+ // Determines if the active report has a history of draft comments while active.
+ let hasDraftHistory;
+
+ // If the active report has not changed and the message has been sent, set the draft history flag to false so LHN can reorder.
+ // Otherwise, if the active report has not changed and the flag was previously true, preserve the state so LHN cannot reorder.
+ // Otherwise, update the flag from the prop value.
+ if (isActiveReportSame && prevState.activeReport.lastMessageTimestamp !== lastMessageTimestamp) {
+ hasDraftHistory = false;
+ } else if (isActiveReportSame && prevState.activeReport.hasDraftHistory) {
+ hasDraftHistory = true;
+ } else {
+ hasDraftHistory = lodashGet(nextProps.reportsWithDraft, `${ONYXKEYS.COLLECTION.REPORTS_WITH_DRAFT}${nextProps.currentlyViewedReportID}`, false);
+ }
+
+ const shouldReorder = SidebarLinks.shouldReorder(nextProps, hasDraftHistory, prevState.orderedReports, prevState.activeReport.reportID, prevState.unreadReports);
const switchingPriorityModes = nextProps.priorityMode !== prevState.priorityMode;
// Build the report options we want to show
@@ -187,7 +222,11 @@ class SidebarLinks extends React.Component {
return {
orderedReports,
priorityMode: nextProps.priorityMode,
- currentlyViewedReportID: nextProps.currentlyViewedReportID,
+ activeReport: {
+ reportID: nextProps.currentlyViewedReportID,
+ hasDraftHistory,
+ lastMessageTimestamp,
+ },
unreadReports: SidebarLinks.getUnreadReports(nextProps.reports || {}),
};
}
diff --git a/src/pages/home/sidebar/SidebarScreen.js b/src/pages/home/sidebar/SidebarScreen.js
index 3d3d06ee5115..158ba45a26ac 100755
--- a/src/pages/home/sidebar/SidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen.js
@@ -43,10 +43,10 @@ class SidebarScreen extends Component {
constructor(props) {
super(props);
- this.onCreateMenuItemSelected = this.onCreateMenuItemSelected.bind(this);
- this.toggleCreateMenu = this.toggleCreateMenu.bind(this);
+ this.hideCreateMenu = this.hideCreateMenu.bind(this);
this.startTimer = this.startTimer.bind(this);
this.navigateToSettings = this.navigateToSettings.bind(this);
+ this.showCreateMenu = this.showCreateMenu.bind(this);
this.state = {
isCreateMenuActive: false,
@@ -58,14 +58,16 @@ class SidebarScreen extends Component {
Timing.start(CONST.TIMING.SIDEBAR_LOADED, true);
const routes = lodashGet(this.props.navigation.getState(), 'routes', []);
- WelcomeAction.show({routes, toggleCreateMenu: this.toggleCreateMenu});
+ WelcomeAction.show({routes, hideCreateMenu: this.hideCreateMenu});
}
/**
- * Method called when a Create Menu item is selected.
+ * Method called when we click the floating action button
*/
- onCreateMenuItemSelected() {
- this.toggleCreateMenu();
+ showCreateMenu() {
+ this.setState({
+ isCreateMenuActive: true,
+ });
}
/**
@@ -76,16 +78,14 @@ class SidebarScreen extends Component {
}
/**
- * Method called when we click the floating action button
- * will trigger the animation
* Method called either when:
* Pressing the floating action button to open the CreateMenu modal
* Selecting an item on CreateMenu or closing it by clicking outside of the modal component
*/
- toggleCreateMenu() {
- this.setState(state => ({
- isCreateMenuActive: !state.isCreateMenuActive,
- }));
+ hideCreateMenu() {
+ this.setState({
+ isCreateMenuActive: false,
+ });
}
/**
@@ -117,14 +117,14 @@ class SidebarScreen extends Component {
accessibilityLabel={this.props.translate('sidebarScreen.fabNewChat')}
accessibilityRole="button"
isActive={this.state.isCreateMenuActive}
- onPress={this.toggleCreateMenu}
+ onPress={this.showCreateMenu}
/>
-
+ {!didScreenTransitionEnd && }
{didScreenTransitionEnd && (
<>
{currentStep === Steps.IOUAmount && (
diff --git a/src/pages/settings/Payments/AddDebitCardPage.js b/src/pages/settings/Payments/AddDebitCardPage.js
index ed44cd10b4d1..e6a8406c1cea 100644
--- a/src/pages/settings/Payments/AddDebitCardPage.js
+++ b/src/pages/settings/Payments/AddDebitCardPage.js
@@ -1,6 +1,7 @@
import React, {Component} from 'react';
import {View} from 'react-native';
import lodashGet from 'lodash/get';
+import lodashEndsWith from 'lodash/endsWith';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
@@ -91,6 +92,7 @@ class DebitCardPage extends Component {
this.submit = this.submit.bind(this);
this.clearErrorAndSetValue = this.clearErrorAndSetValue.bind(this);
this.getErrorText = this.getErrorText.bind(this);
+ this.addOrRemoveSlashToExpiryDate = this.addOrRemoveSlashToExpiryDate.bind(this);
}
/**
@@ -184,6 +186,35 @@ class DebitCardPage extends Component {
}));
}
+ /**
+ * @param {String} inputExpiryDate
+ */
+ addOrRemoveSlashToExpiryDate(inputExpiryDate) {
+ this.setState((prevState) => {
+ let expiryDate = inputExpiryDate;
+
+ // Remove the digit and '/' when backspace is pressed with expiry date ending with '/'
+ if (inputExpiryDate.length < prevState.expirationDate.length
+ && (((inputExpiryDate.length === 3 && lodashEndsWith(inputExpiryDate, '/'))
+ || (inputExpiryDate.length === 2 && lodashEndsWith(prevState.expirationDate, '/'))))) {
+ expiryDate = inputExpiryDate.substring(0, inputExpiryDate.length - 1);
+ } else if (inputExpiryDate.length === 2 && _.indexOf(inputExpiryDate, '/') === -1) {
+ // An Expiry Date was added, so we should append a slash '/'
+ expiryDate = `${inputExpiryDate}/`;
+ } else if (inputExpiryDate.length > 2 && _.indexOf(inputExpiryDate, '/') === -1) {
+ // Expiry Date with MM and YY without slash, hence adding slash(/)
+ expiryDate = `${inputExpiryDate.slice(0, 2)}/${inputExpiryDate.slice(2)}`;
+ }
+ return {
+ expirationDate: expiryDate,
+ errors: {
+ ...prevState.errors,
+ expirationDate: false,
+ },
+ };
+ });
+ }
+
render() {
return (
@@ -217,11 +248,11 @@ class DebitCardPage extends Component {
this.clearErrorAndSetValue('expirationDate', expirationDate)}
value={this.state.expirationDate}
maxLength={7}
errorText={this.getErrorText('expirationDate')}
keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD}
+ onChangeText={this.addOrRemoveSlashToExpiryDate}
/>
@@ -239,7 +270,7 @@ class DebitCardPage extends Component {
label={this.props.translate('addDebitCardPage.billingAddress')}
containerStyles={[styles.mt4]}
value={this.state.addressStreet}
- onChange={(values) => {
+ onInputChange={(values) => {
const renamedFields = {
street: 'addressStreet',
state: 'addressState',
@@ -287,7 +318,7 @@ class DebitCardPage extends Component {
{
+ onInputChange={() => {
this.setState(prevState => ({
acceptedTerms: !prevState.acceptedTerms,
errors: {
diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js
index 9d01deefe9a6..9f1ddd25ea33 100644
--- a/src/pages/settings/Payments/PaymentMethodList.js
+++ b/src/pages/settings/Payments/PaymentMethodList.js
@@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx';
import styles from '../../../styles/styles';
import * as StyleUtils from '../../../styles/StyleUtils';
import MenuItem from '../../../components/MenuItem';
+import Button from '../../../components/Button';
import Text from '../../../components/Text';
import compose from '../../../libs/compose';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
@@ -16,17 +17,12 @@ import bankAccountPropTypes from '../../../components/bankAccountPropTypes';
import * as PaymentUtils from '../../../libs/PaymentUtils';
const MENU_ITEM = 'menuItem';
+const BUTTON = 'button';
const propTypes = {
/** What to do when a menu item is pressed */
onPress: PropTypes.func.isRequired,
- /** Are we loading payments from the server? */
- isLoadingPayments: PropTypes.bool,
-
- /** Is the payment options menu open / active? */
- isAddPaymentMenuActive: PropTypes.bool,
-
/** User's paypal.me username if they have one */
payPalMeUsername: PropTypes.string,
@@ -80,8 +76,6 @@ const defaultProps = {
walletLinkedAccountID: 0,
walletLinkedAccountType: '',
},
- isLoadingPayments: false,
- isAddPaymentMenuActive: false,
shouldShowAddPaymentMethodButton: true,
filterType: '',
actionPaymentMethodType: '',
@@ -121,7 +115,10 @@ class PaymentMethodList extends Component {
* @returns {Array}
*/
getFilteredPaymentMethods() {
- let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(this.props.bankAccountList, this.props.cardList, this.props.payPalMeUsername, this.props.userWallet);
+ // Hide the billing card from the payments menu for now because you can't make it your default method, or delete it
+ const filteredCardList = _.filter(this.props.cardList, card => !card.additionalData.isBillingCard);
+
+ let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(this.props.bankAccountList, filteredCardList, this.props.payPalMeUsername, this.props.userWallet);
if (!_.isEmpty(this.props.filterType)) {
combinedPaymentMethods = _.filter(combinedPaymentMethods, paymentMethod => paymentMethod.accountType === this.props.filterType);
@@ -130,7 +127,7 @@ class PaymentMethodList extends Component {
combinedPaymentMethods = _.map(combinedPaymentMethods, paymentMethod => ({
...paymentMethod,
type: MENU_ITEM,
- onPress: e => this.props.onPress(e, paymentMethod.accountType, paymentMethod.accountData),
+ onPress: e => this.props.onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault),
iconFill: this.isPaymentMethodActive(paymentMethod) ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null,
wrapperStyle: this.isPaymentMethodActive(paymentMethod) ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null,
}));
@@ -159,14 +156,16 @@ class PaymentMethodList extends Component {
}
combinedPaymentMethods.push({
- type: MENU_ITEM,
- title: this.props.translate('paymentMethodList.addPaymentMethod'),
- icon: Expensicons.Plus,
+ type: BUTTON,
+ text: this.props.translate('paymentMethodList.addPaymentMethod'),
+ icon: Expensicons.CreditCard,
+ style: [styles.mh4],
+ iconStyles: [styles.mr4],
onPress: e => this.props.onPress(e),
+ isDisabled: this.props.isLoadingPayments,
+ shouldShowRightIcon: true,
+ success: true,
key: 'addPaymentMethodButton',
- disabled: this.props.isLoadingPayments,
- iconFill: this.props.isAddPaymentMenuActive ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null,
- wrapperStyle: this.props.isAddPaymentMenuActive ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : [],
});
return combinedPaymentMethods;
@@ -209,6 +208,21 @@ class PaymentMethodList extends Component {
/>
);
}
+ if (item.type === BUTTON) {
+ return (
+
+ );
+ }
return (
)}
- {Permissions.canUseWallet(this.props.betas) && !isPayPalMeSelected && (
+ {shouldShowMakeDefaultButton && (
{
this.setState({
@@ -356,7 +359,7 @@ class BasePaymentsPage extends React.Component {
style={[
styles.button,
styles.buttonDanger,
- Permissions.canUseWallet(this.props.betas) && !isPayPalMeSelected && styles.mt4,
+ shouldShowMakeDefaultButton && styles.mt4,
styles.alignSelfCenter,
styles.w100,
]}
@@ -436,10 +439,6 @@ export default compose(
walletTransfer: {
key: ONYXKEYS.WALLET_TRANSFER,
},
- isLoadingPaymentMethods: {
- key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS,
- initWithStoredValues: false,
- },
userWallet: {
key: ONYXKEYS.USER_WALLET,
},
diff --git a/src/pages/settings/PreferencesPage.js b/src/pages/settings/PreferencesPage.js
index 4752dfc291d8..56b928553571 100755
--- a/src/pages/settings/PreferencesPage.js
+++ b/src/pages/settings/PreferencesPage.js
@@ -21,6 +21,7 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize
import compose from '../../libs/compose';
import Picker from '../../components/Picker';
import withEnvironment, {environmentPropTypes} from '../../components/withEnvironment';
+import TestToolMenu from '../../components/TestToolMenu';
const propTypes = {
/** The chat priority mode */
@@ -85,7 +86,7 @@ const PreferencesPage = (props) => {
NameValuePair.set(CONST.NVP.PRIORITY_MODE, mode, ONYXKEYS.NVP_PRIORITY_MODE)
}
items={_.values(priorityModes)}
@@ -99,28 +100,8 @@ const PreferencesPage = (props) => {
- {/* If we are in the staging environment then we have the option to switch from using the staging secure endpoint or the production secure endpoint. This enables QA */}
- {/* and internal testers to take advantage of sandbox environments for 3rd party services like Plaid and Onfido */}
- {props.environment === CONST.ENVIRONMENT.STAGING && (
- <>
-
- Test Preferences
-
-
-
-
- Use Secure Staging Server
-
-
-
-
-
-
- >
- )}
+ {/* If we are in the staging environment then we enable additional test features */}
+ {props.environment === CONST.ENVIRONMENT.STAGING && }
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index cf4ecd2e5ce2..b1cd72d47b9a 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -8,7 +8,6 @@ import moment from 'moment-timezone';
import _ from 'underscore';
import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton';
import Navigation from '../../../libs/Navigation/Navigation';
-import * as OptionsListUtils from '../../../libs/OptionsListUtils';
import ScreenWrapper from '../../../components/ScreenWrapper';
import * as PersonalDetails from '../../../libs/actions/PersonalDetails';
import ROUTES from '../../../ROUTES';
@@ -29,6 +28,7 @@ import CheckboxWithLabel from '../../../components/CheckboxWithLabel';
import AvatarWithImagePicker from '../../../components/AvatarWithImagePicker';
import currentUserPersonalDetailsPropsTypes from './currentUserPersonalDetailsPropsTypes';
import * as ValidationUtils from '../../../libs/ValidationUtils';
+import * as ReportUtils from '../../../libs/reportUtils';
const propTypes = {
/* Onyx Props */
@@ -56,16 +56,19 @@ const defaultProps = {
loginList: [],
};
-const timezones = _.map(moment.tz.names(), timezone => ({
- value: timezone,
- label: timezone,
-}));
+const timezones = _.chain(moment.tz.names())
+ .filter(timezone => !timezone.startsWith('Etc/GMT'))
+ .map(timezone => ({
+ value: timezone,
+ label: timezone,
+ }))
+ .value();
class ProfilePage extends Component {
constructor(props) {
super(props);
- this.defaultAvatar = OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login);
+ this.defaultAvatar = ReportUtils.getDefaultAvatar(this.props.myPersonalDetails.login);
this.state = {
firstName: props.myPersonalDetails.firstName,
@@ -78,7 +81,7 @@ class ProfilePage extends Component {
selectedTimezone: lodashGet(props.myPersonalDetails.timezone, 'selected', CONST.DEFAULT_TIME_ZONE.selected),
isAutomaticTimezone: lodashGet(props.myPersonalDetails.timezone, 'automatic', CONST.DEFAULT_TIME_ZONE.automatic),
logins: this.getLogins(props.loginList),
- avatar: {uri: lodashGet(this.props.myPersonalDetails, 'avatar', OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login))},
+ avatar: {uri: lodashGet(this.props.myPersonalDetails, 'avatar', ReportUtils.getDefaultAvatar(this.props.myPersonalDetails.login))},
isAvatarChanged: false,
};
@@ -152,7 +155,7 @@ class ProfilePage extends Component {
* @param {Object} avatar
*/
updateAvatar(avatar) {
- this.setState({avatar: _.isUndefined(avatar) ? {uri: OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login)} : avatar, isAvatarChanged: true});
+ this.setState({avatar: _.isUndefined(avatar) ? {uri: ReportUtils.getDefaultAvatar(this.props.myPersonalDetails.login)} : avatar, isAvatarChanged: true});
}
/**
@@ -250,7 +253,7 @@ class ProfilePage extends Component {
{
+ onInputChange={(pronouns) => {
const hasSelfSelectedPronouns = pronouns === CONST.PRONOUNS.SELF_SELECT;
this.setState({
pronouns: hasSelfSelectedPronouns ? '' : pronouns,
@@ -288,7 +291,7 @@ class ProfilePage extends Component {
this.setState({selectedTimezone})}
+ onInputChange={selectedTimezone => this.setState({selectedTimezone})}
items={timezones}
isDisabled={this.state.isAutomaticTimezone}
value={this.state.selectedTimezone}
@@ -297,7 +300,7 @@ class ProfilePage extends Component {
diff --git a/src/pages/settings/Security/CloseAccountPage.js b/src/pages/settings/Security/CloseAccountPage.js
index 2e857c543ecc..237892d28287 100644
--- a/src/pages/settings/Security/CloseAccountPage.js
+++ b/src/pages/settings/Security/CloseAccountPage.js
@@ -111,7 +111,7 @@ class CloseAccountPage extends Component {
/>
diff --git a/src/pages/workspace/WorkspaceBankAccountPage.js b/src/pages/workspace/WorkspaceBankAccountPage.js
index 3acaa0df7c85..04a89ca53995 100644
--- a/src/pages/workspace/WorkspaceBankAccountPage.js
+++ b/src/pages/workspace/WorkspaceBankAccountPage.js
@@ -21,6 +21,8 @@ import WorkspaceResetBankAccountModal from './WorkspaceResetBankAccountModal';
import styles from '../../styles/styles';
import CONST from '../../CONST';
import withFullPolicy from './withFullPolicy';
+import Button from '../../components/Button';
+import MenuItem from '../../components/MenuItem';
const propTypes = {
/** ACH data for the withdrawal account actively being set up */
@@ -110,25 +112,27 @@ class WorkspaceBankAccountPage extends React.Component {
{this.props.translate('workspace.bankAccount.youreAlmostDone')}
+
+
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index 16777e17fd16..0aa6dbcd079c 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -262,7 +262,7 @@ class WorkspaceInvitePage extends React.Component {
onBackButtonPress={() => Navigation.goBack()}
/>
-
+ {!didScreenTransitionEnd && }
{didScreenTransitionEnd && (
this.toggleUser(item.login)}
forceTextUnreadStyle
- isDisabled
+ isDisabled={!canBeRemoved}
option={{
text: Str.removeSMSDomain(item.displayName),
alternateText: Str.removeSMSDomain(item.login),
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index 780bf75f487e..3b2ce0417fe3 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -159,7 +159,6 @@ class WorkspaceNewRoomPage extends React.Component {
this.clearErrorAndSetValue('roomName', roomName)}
@@ -173,7 +172,7 @@ class WorkspaceNewRoomPage extends React.Component {
items={this.state.workspaceOptions}
errorText={this.state.errors.policyID}
hasError={Boolean(this.state.errors.policyID)}
- onChange={policyID => this.clearErrorAndSetValue('policyID', policyID)}
+ onInputChange={policyID => this.clearErrorAndSetValue('policyID', policyID)}
/>
@@ -181,7 +180,7 @@ class WorkspaceNewRoomPage extends React.Component {
value={this.state.visibility}
label={this.props.translate('newRoomPage.visibility')}
items={visibilityOptions}
- onChange={visibility => this.setState({visibility})}
+ onInputChange={visibility => this.setState({visibility})}
/>
diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js
index ee9989d86ff3..a0bb45fcbe6b 100644
--- a/src/pages/workspace/WorkspaceSettingsPage.js
+++ b/src/pages/workspace/WorkspaceSettingsPage.js
@@ -168,7 +168,7 @@ class WorkspaceSettingsPage extends React.Component {
this.setState({currency})}
+ onInputChange={currency => this.setState({currency})}
items={this.getCurrencyItems()}
value={this.state.currency}
isDisabled={hasVBA}
diff --git a/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js b/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js
index ddebb7349547..41099e0f54ae 100644
--- a/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js
+++ b/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js
@@ -10,6 +10,7 @@ import Section from '../../../components/Section';
import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
import WorkspaceBillsFirstSection from './WorkspaceBillsFirstSection';
+import Button from '../../../components/Button';
const propTypes = {
/** The policy ID currently being configured */
@@ -25,19 +26,20 @@ const WorkspaceBillsNoVBAView = props => (
Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(props.policyID)),
- icon: Expensicons.Bank,
- shouldShowRightIcon: true,
- iconRight: Expensicons.ArrowRight,
- },
- ]}
>
{props.translate('workspace.bills.unlockNoVBACopy')}
+
>
);
diff --git a/src/pages/workspace/card/WorkspaceCardNoVBAView.js b/src/pages/workspace/card/WorkspaceCardNoVBAView.js
index 2b7f0a46973d..1ad5447df218 100644
--- a/src/pages/workspace/card/WorkspaceCardNoVBAView.js
+++ b/src/pages/workspace/card/WorkspaceCardNoVBAView.js
@@ -10,6 +10,7 @@ import * as Expensicons from '../../../components/Icon/Expensicons';
import * as Illustrations from '../../../components/Icon/Illustrations';
import UnorderedList from '../../../components/UnorderedList';
import Section from '../../../components/Section';
+import Button from '../../../components/Button';
const propTypes = {
/** The policy ID currently being configured */
@@ -22,14 +23,6 @@ const WorkspaceCardNoVBAView = props => (
Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(props.policyID)),
- icon: Expensicons.Bank,
- shouldShowRightIcon: true,
- },
- ]}
>
{props.translate('workspace.card.noVBACopy')}
@@ -43,6 +36,16 @@ const WorkspaceCardNoVBAView = props => (
props.translate('workspace.card.benefit4'),
]}
/>
+
);
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesNoVBAView.js b/src/pages/workspace/invoices/WorkspaceInvoicesNoVBAView.js
index e040bcd331e4..4591ce4d0e91 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesNoVBAView.js
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesNoVBAView.js
@@ -10,6 +10,7 @@ import Section from '../../../components/Section';
import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
import WorkspaceInvoicesFirstSection from './WorkspaceInvoicesFirstSection';
+import Button from '../../../components/Button';
const propTypes = {
/** The policy ID currently being configured */
@@ -25,19 +26,20 @@ const WorkspaceInvoicesNoVBAView = props => (
Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(props.policyID)),
- icon: Expensicons.Bank,
- shouldShowRightIcon: true,
- iconRight: Expensicons.ArrowRight,
- },
- ]}
>
{props.translate('workspace.invoices.unlockNoVBACopy')}
+
>
);
diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js
index 6e68c98488cd..f36f63b87698 100644
--- a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js
+++ b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js
@@ -21,6 +21,7 @@ import ONYXKEYS from '../../../ONYXKEYS';
import * as Policy from '../../../libs/actions/Policy';
import withFullPolicy from '../withFullPolicy';
import CONST from '../../../CONST';
+import Button from '../../../components/Button';
const propTypes = {
/** The policy ID currently being configured */
@@ -181,7 +182,7 @@ class WorkspaceReimburseNoVBAView extends React.Component {
label={this.props.translate('workspace.reimburse.trackDistanceUnit')}
items={this.unitItems}
value={this.state.unitValue}
- onChange={value => this.setUnit(value)}
+ onInputChange={value => this.setUnit(value)}
/>
@@ -190,18 +191,20 @@ class WorkspaceReimburseNoVBAView extends React.Component {
Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(this.props.policyID)),
- icon: Expensicons.Bank,
- shouldShowRightIcon: true,
- },
- ]}
>
{this.props.translate('workspace.reimburse.unlockNoVBACopy')}
+
>
);
diff --git a/src/pages/workspace/travel/WorkspaceTravelNoVBAView.js b/src/pages/workspace/travel/WorkspaceTravelNoVBAView.js
index f50d7c49d00c..6afc2837aa05 100644
--- a/src/pages/workspace/travel/WorkspaceTravelNoVBAView.js
+++ b/src/pages/workspace/travel/WorkspaceTravelNoVBAView.js
@@ -9,6 +9,7 @@ import * as Illustrations from '../../../components/Icon/Illustrations';
import Section from '../../../components/Section';
import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
+import Button from '../../../components/Button';
const propTypes = {
/** The policy ID currently being configured */
@@ -22,19 +23,20 @@ const WorkspaceTravelNoVBAView = props => (
Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(props.policyID)),
- icon: Expensicons.Bank,
- shouldShowRightIcon: true,
- iconRight: Expensicons.ArrowRight,
- },
- ]}
>
{props.translate('workspace.travel.noVBACopy')}
+
>
);
diff --git a/src/stories/AddressSearch.stories.js b/src/stories/AddressSearch.stories.js
index 6f03680c47f7..b19d3ac46b3a 100644
--- a/src/stories/AddressSearch.stories.js
+++ b/src/stories/AddressSearch.stories.js
@@ -20,7 +20,7 @@ const Template = (args) => {
return (
setValue(street)}
+ onInputChange={({street}) => setValue(street)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...args}
/>
diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js
index 7de80c6192af..262a9f3a1c90 100644
--- a/src/stories/Form.stories.js
+++ b/src/stories/Form.stories.js
@@ -1,6 +1,7 @@
import React, {useState} from 'react';
import {View} from 'react-native';
import TextInput from '../components/TextInput';
+import Picker from '../components/Picker';
import AddressSearch from '../components/AddressSearch';
import Form from '../components/Form';
import * as FormActions from '../libs/actions/FormActions';
@@ -16,12 +17,15 @@ import Text from '../components/Text';
const story = {
title: 'Components/Form',
component: Form,
- subcomponents: {TextInput, AddressSearch, CheckboxWithLabel},
+ subcomponents: {
+ TextInput,
+ AddressSearch,
+ CheckboxWithLabel,
+ Picker,
+ },
};
const Template = (args) => {
- const [isChecked, setIsChecked] = useState(args.draftValues.checkbox);
-
// Form consumes data from Onyx, so we initialize Onyx with the necessary data here
FormActions.setIsSubmitting(args.formID, args.formState.isSubmitting);
FormActions.setServerErrorMessage(args.formID, args.formState.serverErrorMessage);
@@ -50,12 +54,52 @@ const Template = (args) => {
containerStyles={[styles.mt4]}
isFormInput
/>
+
+
+
+
{ setIsChecked(prev => !prev); }}
isFormInput
shouldSaveDraft
LabelComponent={() => (
@@ -114,6 +158,12 @@ const defaultArgs = {
if (!values.accountNumber) {
errors.accountNumber = 'Please enter an account number';
}
+ if (!values.pickFruit) {
+ errors.pickFruit = 'Please select a fruit';
+ }
+ if (!values.pickAnotherFruit) {
+ errors.pickAnotherFruit = 'Please select a fruit';
+ }
if (!values.checkbox) {
errors.checkbox = 'You must accept the Terms of Service to continue';
}
@@ -132,6 +182,8 @@ const defaultArgs = {
draftValues: {
routingNumber: '00001',
accountNumber: '1111222233331111',
+ pickFruit: 'orange',
+ pickAnotherFruit: 'apple',
checkbox: false,
},
};
@@ -139,7 +191,17 @@ const defaultArgs = {
Default.args = defaultArgs;
Loading.args = {...defaultArgs, formState: {isSubmitting: true}};
ServerError.args = {...defaultArgs, formState: {isSubmitting: false, serverErrorMessage: 'There was an unexpected error. Please try again later.'}};
-InputError.args = {...defaultArgs, draftValues: {routingNumber: '', accountNumber: '', checkbox: false}};
+InputError.args = {
+ ...defaultArgs,
+ draftValues: {
+ routingNumber: '',
+ accountNumber: '',
+ pickFruit: '',
+ pickAnotherFruit: '',
+ checkbox: false,
+ },
+};
+
WithNativeEventHandler.args = {...defaultArgs, draftValues: {routingNumber: '', accountNumber: ''}};
export default story;
diff --git a/src/stories/Picker.stories.js b/src/stories/Picker.stories.js
new file mode 100644
index 000000000000..dd26adebc484
--- /dev/null
+++ b/src/stories/Picker.stories.js
@@ -0,0 +1,97 @@
+import React from 'react';
+import Picker from '../components/Picker';
+
+/**
+ * We use the Component Story Format for writing stories. Follow the docs here:
+ *
+ * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
+ */
+const story = {
+ title: 'Components/Picker',
+ component: Picker,
+};
+
+// eslint-disable-next-line react/jsx-props-no-spreading
+const Template = args => ;
+
+// Arguments can be passed to the component by binding
+// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
+
+const Default = Template.bind({});
+Default.args = {
+ label: 'Default picker',
+ name: 'Default',
+ onInputChange: () => {},
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+const PickerWithValue = Template.bind({});
+PickerWithValue.args = {
+ label: 'Picker with defined value',
+ name: 'Picker with defined value',
+ onInputChange: () => {},
+ value: 'apple',
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+const ErrorStory = Template.bind({});
+ErrorStory.args = {
+ label: 'Picker with error',
+ name: 'PickerWithError',
+ errorText: 'This field has an error.',
+ onInputChange: () => {},
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+const Disabled = Template.bind({});
+Disabled.args = {
+ label: 'Picker disabled',
+ name: 'Disabled',
+ isDisabled: true,
+ onInputChange: () => {},
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+export default story;
+export {
+ Default,
+ PickerWithValue,
+ ErrorStory,
+ Disabled,
+};
diff --git a/src/styles/cardStyles/index.js b/src/styles/cardStyles/index.js
new file mode 100644
index 000000000000..4759700c6f92
--- /dev/null
+++ b/src/styles/cardStyles/index.js
@@ -0,0 +1,16 @@
+import variables from '../variables';
+
+/**
+ * Get card style for cardStyleInterpolator
+ * @param {Boolean} isSmallScreenWidth
+ * @param {Number} screenWidth
+ * @returns {Object}
+ */
+export default function getCardStyles(isSmallScreenWidth, screenWidth) {
+ return {
+ position: 'fixed',
+ width: isSmallScreenWidth ? screenWidth : variables.sideBarWidth,
+ right: 0,
+ height: '100%',
+ };
+}
diff --git a/src/styles/cardStyles/index.native.js b/src/styles/cardStyles/index.native.js
new file mode 100644
index 000000000000..fef33f500708
--- /dev/null
+++ b/src/styles/cardStyles/index.native.js
@@ -0,0 +1,3 @@
+export default function getCardStyles() {
+ return {};
+}
diff --git a/src/styles/overflowXHidden/index.js b/src/styles/overflowXHidden/index.js
new file mode 100644
index 000000000000..6cdd34a05eb0
--- /dev/null
+++ b/src/styles/overflowXHidden/index.js
@@ -0,0 +1,3 @@
+export default {
+ overflowX: 'hidden',
+};
diff --git a/src/styles/overflowXHidden/index.native.js b/src/styles/overflowXHidden/index.native.js
new file mode 100644
index 000000000000..ff8b4c56321a
--- /dev/null
+++ b/src/styles/overflowXHidden/index.native.js
@@ -0,0 +1 @@
+export default {};
diff --git a/src/styles/pointerEventsNone/index.js b/src/styles/pointerEventsNone/index.js
new file mode 100644
index 000000000000..93002d269d01
--- /dev/null
+++ b/src/styles/pointerEventsNone/index.js
@@ -0,0 +1,3 @@
+export default {
+ pointerEvents: 'none',
+};
diff --git a/src/styles/pointerEventsNone/index.native.js b/src/styles/pointerEventsNone/index.native.js
new file mode 100644
index 000000000000..ff8b4c56321a
--- /dev/null
+++ b/src/styles/pointerEventsNone/index.native.js
@@ -0,0 +1 @@
+export default {};
diff --git a/src/styles/styles.js b/src/styles/styles.js
index d095b16fd49a..caf02a03c31c 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -16,6 +16,8 @@ import positioning from './utilities/positioning';
import codeStyles from './codeStyles';
import visibility from './utilities/visibility';
import optionAlternateTextPlatformStyles from './optionAlternateTextPlatformStyles';
+import pointerEventsNone from './pointerEventsNone';
+import overflowXHidden from './overflowXHidden';
const picker = {
backgroundColor: 'transparent',
@@ -379,6 +381,16 @@ const styles = {
backgroundColor: themeColors.buttonDefaultBG,
},
+ buttonExtraLarge: {
+ borderRadius: variables.componentBorderRadius,
+ height: variables.componentSizeExtraLarge,
+ paddingTop: 12,
+ paddingRight: 18,
+ paddingBottom: 12,
+ paddingLeft: 18,
+ backgroundColor: themeColors.buttonDefaultBG,
+ },
+
buttonSmallText: {
fontSize: variables.fontSizeSmall,
fontFamily: fontFamily.GTA_BOLD,
@@ -400,6 +412,13 @@ const styles = {
textAlign: 'center',
},
+ buttonExtraLargeText: {
+ fontSize: variables.fontSizeMedium,
+ fontFamily: fontFamily.GTA_BOLD,
+ fontWeight: fontWeightBold,
+ textAlign: 'center',
+ },
+
buttonSuccess: {
backgroundColor: themeColors.buttonSuccessBG,
borderWidth: 0,
@@ -550,7 +569,7 @@ const styles = {
iconContainer: {
top: 8,
right: 9,
- pointerEvents: 'none',
+ ...pointerEventsNone,
},
icon: {
width: variables.iconSizeExtraSmall,
@@ -1342,7 +1361,7 @@ const styles = {
borderRadius: 0,
height: 'auto',
lineHeight: 20,
- overflowX: 'hidden',
+ ...overflowXHidden,
// On Android, multiline TextInput with height: 'auto' will show extra padding unless they are configured with
// paddingVertical: 0, alignSelf: 'center', and textAlignVertical: 'center'
@@ -1666,9 +1685,7 @@ const styles = {
borderColor: themeColors.border,
},
- pointerEventsNone: {
- pointerEvents: 'none',
- },
+ pointerEventsNone,
headerBar: {
overflow: 'hidden',
@@ -1691,14 +1708,17 @@ const styles = {
backgroundColor: themeColors.modalBackground,
},
PDFView: {
- flex: 1,
+ // `display: grid` is not supported in native platforms!
+ // It's being used on Web/Desktop only to vertically center short PDFs,
+ // while preventing the overflow of the top of long PDF files.
+ display: 'grid',
backgroundColor: themeColors.modalBackground,
width: '100%',
height: '100%',
- flexDirection: 'row',
justifyContent: 'center',
overflow: 'hidden',
overflowY: 'auto',
+ alignItems: 'center',
},
modalCenterContentContainer: {
diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js
index e5ddcee0c3f0..6ba2830de7bf 100644
--- a/src/styles/utilities/spacing.js
+++ b/src/styles/utilities/spacing.js
@@ -37,6 +37,10 @@ export default {
marginHorizontal: 12,
},
+ mh4: {
+ marginHorizontal: 16,
+ },
+
mh5: {
marginHorizontal: 20,
},
diff --git a/src/styles/variables.js b/src/styles/variables.js
index 5ffd6bf29131..4302947ac879 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -4,6 +4,7 @@ export default {
componentSizeNormal: 40,
inputComponentSizeNormal: 42,
componentSizeLarge: 52,
+ componentSizeExtraLarge: 64,
componentBorderRadius: 8,
componentBorderRadiusSmall: 4,
componentBorderRadiusNormal: 8,
@@ -19,6 +20,7 @@ export default {
fontSizeExtraSmall: 9,
fontSizeLabel: 13,
fontSizeNormal: 15,
+ fontSizeMedium: 16,
fontSizeLarge: 17,
fontSizeHero: 36,
fontSizeh1: 19,
diff --git a/tests/actions/ReimbursementAccountTest.js b/tests/actions/ReimbursementAccountTest.js
index bb6717c466ac..10e01d5d9d8d 100644
--- a/tests/actions/ReimbursementAccountTest.js
+++ b/tests/actions/ReimbursementAccountTest.js
@@ -33,6 +33,8 @@ Onyx.connect({
callback: val => reimbursementAccount = val,
});
+beforeAll(() => Onyx.init());
+
beforeEach(() => Onyx.clear()
.then(() => {
NetworkStore.setHasReadRequiredDataFromStorage(true);
diff --git a/tests/unit/GithubUtilsTest.js b/tests/unit/GithubUtilsTest.js
index ef8818a293ac..a5c8e8a2687f 100644
--- a/tests/unit/GithubUtilsTest.js
+++ b/tests/unit/GithubUtilsTest.js
@@ -68,6 +68,8 @@ describe('GithubUtils', () => {
url: 'https://api.github.com/repos/Andrew-Test-Org/Public-Test-Repo/issues/29',
number: 29,
deployBlockers: [],
+ isTimingDashboardChecked: false,
+ isFirebaseChecked: false,
};
const expectedResponseWithDeployBlockers = {...baseExpectedResponse};
expectedResponseWithDeployBlockers.deployBlockers = [
@@ -375,6 +377,11 @@ describe('GithubUtils', () => {
const lineBreakDouble = '\r\n\r\n';
const indent = ' ';
const assignOctocatHubot = ' - @octocat @hubot';
+ const deployerVerificationsHeader = '\r\n**Deployer verifications:**';
+ // eslint-disable-next-line max-len
+ const timingDashboardVerification = 'I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.';
+ // eslint-disable-next-line max-len
+ const firebaseVerification = 'I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.';
// Valid output which will be reused in the deploy blocker tests
const allVerifiedExpectedOutput = `${baseExpectedOutput}`
@@ -394,6 +401,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
@@ -409,6 +419,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
@@ -424,6 +437,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${listStart}${basePRList[1]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
@@ -433,7 +449,11 @@ describe('GithubUtils', () => {
githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList)
.then((issueBody) => {
expect(issueBody).toBe(
- `${allVerifiedExpectedOutput}${lineBreakDouble}${ccApplauseLeads}`,
+ `${allVerifiedExpectedOutput}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ + `${lineBreakDouble}${ccApplauseLeads}`,
);
})
));
@@ -446,6 +466,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${deployBlockerHeader}`
+ `${lineBreak}${openCheckbox}${baseDeployBlockerList[0]}`
+ `${lineBreak}${openCheckbox}${baseDeployBlockerList[1]}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
@@ -459,6 +482,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${deployBlockerHeader}`
+ `${lineBreak}${closedCheckbox}${baseDeployBlockerList[0]}`
+ `${lineBreak}${openCheckbox}${baseDeployBlockerList[1]}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
@@ -477,6 +503,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${deployBlockerHeader}`
+ `${lineBreak}${closedCheckbox}${baseDeployBlockerList[0]}`
+ `${lineBreak}${closedCheckbox}${baseDeployBlockerList[1]}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
@@ -495,6 +524,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${internalQAHeader}`
+ `${lineBreak}${openCheckbox}${internalQAPRList[0]}${assignOctocatHubot}`
+ `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
@@ -513,6 +545,9 @@ describe('GithubUtils', () => {
+ `${lineBreakDouble}${internalQAHeader}`
+ `${lineBreak}${closedCheckbox}${internalQAPRList[0]}${assignOctocatHubot}`
+ `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
);
})
diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js
index 3a05284dda72..cad4be39d573 100644
--- a/tests/unit/NetworkTest.js
+++ b/tests/unit/NetworkTest.js
@@ -522,3 +522,40 @@ test('persisted request can trigger reauthentication for anything retryable', ()
expect(commandName3).toBe('Mock');
});
});
+
+test('several actions made while offline will get added in the order they are created', () => {
+ // Given offline state where all requests will eventualy succeed without issue
+ const xhr = jest.spyOn(HttpUtils, 'xhr')
+ .mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS});
+
+ return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})
+ .then(() => {
+ // When we queue 6 persistable commands
+ Network.post('MockCommand', {content: 'value1', persist: true});
+ Network.post('MockCommand', {content: 'value2', persist: true});
+ Network.post('MockCommand', {content: 'value3', persist: true});
+ Network.post('MockCommand', {content: 'not-persisted'});
+ Network.post('MockCommand', {content: 'value4', persist: true});
+ Network.post('MockCommand', {content: 'value5', persist: true});
+ Network.post('MockCommand', {content: 'value6', persist: true});
+
+ return waitForPromisesToResolve();
+ })
+ .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))
+ .then(waitForPromisesToResolve)
+ .then(() => {
+ // Then expect only 6 calls to have been made and for them to be made in the order that we made them
+ // and the non-persistable request isn't one of them
+ expect(xhr.mock.calls.length).toBe(6);
+ expect(xhr.mock.calls[0][1].content).toBe('value1');
+ expect(xhr.mock.calls[1][1].content).toBe('value2');
+ expect(xhr.mock.calls[2][1].content).toBe('value3');
+ expect(xhr.mock.calls[3][1].content).toBe('value4');
+ expect(xhr.mock.calls[4][1].content).toBe('value5');
+ expect(xhr.mock.calls[5][1].content).toBe('value6');
+
+ // Move main queue forward so it processes the "read" request
+ jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS);
+ expect(xhr.mock.calls[6][1].content).toBe('not-persisted');
+ });
+});
diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js
index dd4013416391..3fb6441c6602 100644
--- a/tests/unit/OptionsListUtilsTest.js
+++ b/tests/unit/OptionsListUtilsTest.js
@@ -238,6 +238,15 @@ describe('OptionsListUtils', () => {
},
};
+ const PERSONAL_DETAILS_WITH_PERIODS = {
+ ...PERSONAL_DETAILS,
+
+ 'barry.allen@expensify.com': {
+ displayName: 'The Flash',
+ login: 'barry.allen@expensify.com',
+ },
+ };
+
// Set the currently logged in user, report data, and personal details
beforeAll(() => {
Onyx.init({
@@ -277,6 +286,13 @@ describe('OptionsListUtils', () => {
// Then we get both values with the pinned value still on top
expect(results.recentReports.length).toBe(2);
expect(results.recentReports[0].text).toBe('Mister Fantastic');
+
+ // When we filter again but provide a searchValue that should match with periods
+ results = OptionsListUtils.getSearchOptions(REPORTS, PERSONAL_DETAILS_WITH_PERIODS, 'barryallen@expensify.com');
+
+ // Then we expect to have the personal detail with period filtered
+ expect(results.recentReports.length).toBe(1);
+ expect(results.recentReports[0].text).toBe('The Flash');
});
it('getNewChatOptions()', () => {
@@ -457,6 +473,13 @@ describe('OptionsListUtils', () => {
expect(results.personalDetails.length).toBe(0);
expect(results.userToInvite).not.toBe(null);
+ // When we add a search term for which exist options for it excluding its period.
+ results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'peter.parker@expensify.com');
+
+ // Then we will have an options at all and there should be a userToInvite too.
+ expect(results.recentReports.length).toBe(1);
+ expect(results.userToInvite).not.toBe(null);
+
// When we add a search term for which no options exist and the searchValue itself
// is a potential phone number without country code added
results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '5005550006');
@@ -652,4 +675,102 @@ describe('OptionsListUtils', () => {
// Spider-Man report name is last report and has unread message
expect(results.recentReports[8].login).toBe('peterparker@expensify.com');
}));
+
+ it('getSidebarOptions() with empty policyExpenseChats and defaultRooms', () => {
+ const reportsWithEmptyChatRooms = {
+ // This report is a policyExpenseChat without any messages in it (i.e. no lastMessageTimestamp)
+ 10: {
+ chatType: 'policyExpenseChat',
+ hasOutstandingIOU: false,
+ isOwnPolicyExpenseChat: true,
+ isPinned: false,
+ lastMessageTimestamp: 0,
+ lastVisitedTimestamp: 1610666739302,
+ participants: ['test3@instantworkspace.com'],
+ policyID: 'Whatever',
+ reportID: 10,
+ reportName: "Someone's workspace",
+ unreadActionCount: 0,
+ visibility: undefined,
+ },
+
+ // This is an archived version of the above policyExpenseChat
+ 11: {
+ chatType: 'policyExpenseChat',
+ hasOutstandingIOU: false,
+ isOwnPolicyExpenseChat: true,
+ isPinned: false,
+ lastMessageTimestamp: 0,
+ lastVisitedTimestamp: 1610666739302,
+ participants: ['test3@instantworkspace.com'],
+ policyID: 'Whatever',
+ reportID: 11,
+ reportName: "Someone's workspace",
+ unreadActionCount: 0,
+ visibility: undefined,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS.CLOSED,
+ },
+
+ // This report is a defaultRoom without any messages in it (i.e. no lastMessageTimestamp)
+ 12: {
+ chatType: 'policyAdmins',
+ hasOutstandingIOU: false,
+ isPinned: false,
+ lastMessageTimestamp: 0,
+ lastVisitedTimestamp: 1610666739302,
+ participants: ['test3@instantworkspace.com'],
+ policyID: 'Whatever',
+ reportID: 12,
+ reportName: '#admins',
+ unreadActionCount: 0,
+ visibility: undefined,
+ },
+
+ // This is an archived version of the above defaultRoom
+ 13: {
+ chatType: 'policyAdmins',
+ hasOutstandingIOU: false,
+ isPinned: false,
+ lastMessageTimestamp: 0,
+ lastVisitedTimestamp: 1610666739302,
+ participants: ['test3@instantworkspace.com'],
+ policyID: 'Whatever',
+ reportID: 13,
+ reportName: '#admins',
+ unreadActionCount: 0,
+ visibility: undefined,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS.CLOSED,
+ },
+ };
+
+ // First we call getSidebarOptions() with no search value and default priority mode
+ let results = OptionsListUtils.getSidebarOptions(
+ reportsWithEmptyChatRooms,
+ PERSONAL_DETAILS,
+ 0,
+ CONST.PRIORITY_MODE.DEFAULT,
+ );
+
+ // Then expect all of the reports to be shown except the archived policyExpenseChats and defaultRooms
+ expect(results.recentReports.length).toBe(_.size(reportsWithEmptyChatRooms) - 2);
+
+ expect(results.recentReports[0].isPolicyExpenseChat).toBe(true);
+ expect(results.recentReports[0].text).toBe("Someone's workspace");
+
+ expect(results.recentReports[1].isChatRoom).toBe(true);
+ expect(results.recentReports[1].text).toBe('#admins');
+
+ // Now we call getSidebarOptions() with no search value and GSD priority mode
+ results = OptionsListUtils.getSidebarOptions(
+ reportsWithEmptyChatRooms,
+ PERSONAL_DETAILS,
+ 0,
+ CONST.PRIORITY_MODE.GSD,
+ );
+
+ // None of the chats should be here since they've all been read
+ expect(results.recentReports.length).toBe(0);
+ });
});
diff --git a/tests/unit/createOrUpdateStagingDeployTest.js b/tests/unit/createOrUpdateStagingDeployTest.js
index 90307c3f469c..3d9d5400e525 100644
--- a/tests/unit/createOrUpdateStagingDeployTest.js
+++ b/tests/unit/createOrUpdateStagingDeployTest.js
@@ -102,6 +102,11 @@ const closedCheckbox = '- [x] ';
const listStart = '- ';
const QA = 'QA';
const accessibility = 'Accessibility';
+const deployerVerificationsHeader = '\r\n**Deployer verifications:**';
+// eslint-disable-next-line max-len
+const timingDashboardVerification = 'I checked the [App Timing Dashboard](https://graphs.expensify.com/grafana/d/yj2EobAGz/app-timing?orgId=1) and verified this release does not cause a noticeable performance regression.';
+// eslint-disable-next-line max-len
+const firebaseVerification = 'I checked [Firebase Crashlytics](https://console.firebase.google.com/u/0/project/expensify-chat/crashlytics/app/android:com.expensify.chat/issues?state=open&time=last-seven-days&tag=all) and verified that this release does not introduce any new crashes.';
const ccApplauseLeads = 'cc @Expensify/applauseleads\r\n';
const deployBlockerHeader = '\r\n**Deploy Blockers:**';
const lineBreak = '\r\n';
@@ -169,6 +174,9 @@ describe('createOrUpdateStagingDeployCash', () => {
+ `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[7]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
});
});
@@ -189,6 +197,9 @@ describe('createOrUpdateStagingDeployCash', () => {
+ `${lineBreak}${openCheckbox}${basePRList[5]}`
+ `${lineBreak}${openCheckbox}${basePRList[8]}`
+ `${lineBreak}${closedCheckbox}${basePRList[9]}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${closedCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${closedCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
state: 'open',
};
@@ -285,12 +296,23 @@ describe('createOrUpdateStagingDeployCash', () => {
+ `${lineBreak}${closedCheckbox}${basePRList[9]}`
+ `${lineBreak}${openCheckbox}${basePRList[10]}`
+ `${lineBreak}${openCheckbox}${basePRList[11]}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+
+ // Note: these will be unchecked with a new app version, and that's intentional
+ + `${lineBreak}${openCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${openCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
});
});
});
test('without NPM_VERSION input, just a new deploy blocker', () => {
+ mockGetInput.mockImplementation((arg) => {
+ if (arg !== 'GITHUB_TOKEN') {
+ return;
+ }
+ return 'fake_token';
+ });
mockGetPullRequestsMergedBetween.mockImplementation((fromRef, toRef) => {
if (fromRef === '1.0.1-0' && toRef === '1.0.2-2') {
return [
@@ -335,7 +357,7 @@ describe('createOrUpdateStagingDeployCash', () => {
// eslint-disable-next-line max-len
html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`,
// eslint-disable-next-line max-len
- body: `${baseExpectedOutput('1.0.2-2')}`
+ body: `${baseExpectedOutput('1.0.2-1')}`
+ `${lineBreakDouble}${listStart}${basePRList[5]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[6]}${lineBreak}${indent}${closedCheckbox}${QA}${lineBreak}${indent}${closedCheckbox}${accessibility}`
+ `${lineBreakDouble}${listStart}${basePRList[7]}${lineBreak}${indent}${openCheckbox}${QA}${lineBreak}${indent}${openCheckbox}${accessibility}`
@@ -345,6 +367,9 @@ describe('createOrUpdateStagingDeployCash', () => {
+ `${lineBreak}${closedCheckbox}${basePRList[9]}`
+ `${lineBreak}${openCheckbox}${baseIssueList[0]}`
+ `${lineBreak}${openCheckbox}${baseIssueList[1]}`
+ + `${lineBreak}${deployerVerificationsHeader}`
+ + `${lineBreak}${closedCheckbox}${timingDashboardVerification}`
+ + `${lineBreak}${closedCheckbox}${firebaseVerification}`
+ `${lineBreakDouble}${ccApplauseLeads}`,
});
});
diff --git a/tests/unit/enhanceParametersTest.js b/tests/unit/enhanceParametersTest.js
index a4fc9f70a9cf..50d9fdbee2ed 100644
--- a/tests/unit/enhanceParametersTest.js
+++ b/tests/unit/enhanceParametersTest.js
@@ -19,6 +19,7 @@ test('Enhance parameters adds correct parameters for Log command with no authTok
testParameter: 'test',
api_setCookie: false,
email,
+ platform: 'ios',
referer: CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER,
});
});
@@ -37,6 +38,7 @@ test('Enhance parameters adds correct parameters for a command that requires aut
testParameter: 'test',
api_setCookie: false,
email,
+ platform: 'ios',
authToken,
referer: CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER,
});