++ if (!this.preserveMultipleSlashes) {
++ for (let i = 1; i < parts.length - 1; i++) {
++ const p = parts[i];
++ // don't squeeze out UNC patterns
++ if (i === 1 && p === '' && parts[0] === '')
++ continue;
++ if (p === '.' || p === '') {
++ didSomething = true;
++ parts.splice(i, 1);
++ i--;
++ }
++ }
++ if (parts[0] === '.' &&
++ parts.length === 2 &&
++ (parts[1] === '.' || parts[1] === '')) {
++ didSomething = true;
++ parts.pop();
++ }
++ }
++ // //../ -> /
++ let dd = 0;
++ while (-1 !== (dd = parts.indexOf('..', dd + 1))) {
++ const p = parts[dd - 1];
++ if (p && p !== '.' && p !== '..' && p !== '**') {
++ didSomething = true;
++ parts.splice(dd - 1, 2);
++ dd -= 2;
++ }
++ }
++ } while (didSomething);
++ return parts.length === 0 ? [''] : parts;
++ }
++ // First phase: single-pattern processing
++ // is 1 or more portions
++ // is 1 or more portions
++ // is any portion other than ., .., '', or **
++ // is . or ''
++ //
++ // **/.. is *brutal* for filesystem walking performance, because
++ // it effectively resets the recursive walk each time it occurs,
++ // and ** cannot be reduced out by a .. pattern part like a regexp
++ // or most strings (other than .., ., and '') can be.
++ //
++ // /**/..//
/ -> {/..//
/,/**//
/}
++ // // -> /
++ // //../ -> /
++ // **/**/ -> **/
++ //
++ // **/*/ -> */**/ <== not valid because ** doesn't follow
++ // this WOULD be allowed if ** did follow symlinks, or * didn't
++ firstPhasePreProcess(globParts) {
++ let didSomething = false;
++ do {
++ didSomething = false;
++ // /**/..//
/ -> {/..//
/,/**//
/}
++ for (let parts of globParts) {
++ let gs = -1;
++ while (-1 !== (gs = parts.indexOf('**', gs + 1))) {
++ let gss = gs;
++ while (parts[gss + 1] === '**') {
++ // /**/**/ -> /**/
++ gss++;
++ }
++ // eg, if gs is 2 and gss is 4, that means we have 3 **
++ // parts, and can remove 2 of them.
++ if (gss > gs) {
++ parts.splice(gs + 1, gss - gs);
++ }
++ let next = parts[gs + 1];
++ const p = parts[gs + 2];
++ const p2 = parts[gs + 3];
++ if (next !== '..')
++ continue;
++ if (!p ||
++ p === '.' ||
++ p === '..' ||
++ !p2 ||
++ p2 === '.' ||
++ p2 === '..') {
++ continue;
++ }
++ didSomething = true;
++ // edit parts in place, and push the new one
++ parts.splice(gs, 1);
++ const other = parts.slice(0);
++ other[gs] = '**';
++ globParts.push(other);
++ gs--;
++ }
++ // // -> /
++ if (!this.preserveMultipleSlashes) {
++ for (let i = 1; i < parts.length - 1; i++) {
++ const p = parts[i];
++ // don't squeeze out UNC patterns
++ if (i === 1 && p === '' && parts[0] === '')
++ continue;
++ if (p === '.' || p === '') {
++ didSomething = true;
++ parts.splice(i, 1);
++ i--;
++ }
++ }
++ if (parts[0] === '.' &&
++ parts.length === 2 &&
++ (parts[1] === '.' || parts[1] === '')) {
++ didSomething = true;
++ parts.pop();
++ }
++ }
++ // //../ -> /
++ let dd = 0;
++ while (-1 !== (dd = parts.indexOf('..', dd + 1))) {
++ const p = parts[dd - 1];
++ if (p && p !== '.' && p !== '..' && p !== '**') {
++ didSomething = true;
++ const needDot = dd === 1 && parts[dd + 1] === '**';
++ const splin = needDot ? ['.'] : [];
++ parts.splice(dd - 1, 2, ...splin);
++ if (parts.length === 0)
++ parts.push('');
++ dd -= 2;
++ }
++ }
++ }
++ } while (didSomething);
++ return globParts;
++ }
++ // second phase: multi-pattern dedupes
++ // {/*/,//} -> /*/
++ // {/,/} -> /
++ // {/**/,/} -> /**/
++ //
++ // {/**/,/**//} -> /**/
++ // ^-- not valid because ** doens't follow symlinks
++ secondPhasePreProcess(globParts) {
++ for (let i = 0; i < globParts.length - 1; i++) {
++ for (let j = i + 1; j < globParts.length; j++) {
++ const matched = this.partsMatch(globParts[i], globParts[j], !this.preserveMultipleSlashes);
++ if (matched) {
++ globParts[i] = [];
++ globParts[j] = matched;
++ break;
++ }
++ }
++ }
++ return globParts.filter(gs => gs.length);
++ }
++ partsMatch(a, b, emptyGSMatch = false) {
++ let ai = 0;
++ let bi = 0;
++ let result = [];
++ let which = '';
++ while (ai < a.length && bi < b.length) {
++ if (a[ai] === b[bi]) {
++ result.push(which === 'b' ? b[bi] : a[ai]);
++ ai++;
++ bi++;
++ }
++ else if (emptyGSMatch && a[ai] === '**' && b[bi] === a[ai + 1]) {
++ result.push(a[ai]);
++ ai++;
++ }
++ else if (emptyGSMatch && b[bi] === '**' && a[ai] === b[bi + 1]) {
++ result.push(b[bi]);
++ bi++;
++ }
++ else if (a[ai] === '*' &&
++ b[bi] &&
++ (this.options.dot || !b[bi].startsWith('.')) &&
++ b[bi] !== '**') {
++ if (which === 'b')
++ return false;
++ which = 'a';
++ result.push(a[ai]);
++ ai++;
++ bi++;
++ }
++ else if (b[bi] === '*' &&
++ a[ai] &&
++ (this.options.dot || !a[ai].startsWith('.')) &&
++ a[ai] !== '**') {
++ if (which === 'a')
++ return false;
++ which = 'b';
++ result.push(b[bi]);
++ ai++;
++ bi++;
++ }
++ else {
++ return false;
++ }
++ }
++ // if we fall out of the loop, it means they two are identical
++ // as long as their lengths match
++ return a.length === b.length && result;
++ }
++ parseNegate() {
++ if (this.nonegate)
++ return;
++ const pattern = this.pattern;
++ let negate = false;
++ let negateOffset = 0;
++ for (let i = 0; i < pattern.length && pattern.charAt(i) === '!'; i++) {
++ negate = !negate;
++ negateOffset++;
++ }
++ if (negateOffset)
++ this.pattern = pattern.slice(negateOffset);
++ this.negate = negate;
++ }
++ // set partial to true to test if, for example,
++ // "/a/b" matches the start of "/*/b/*/d"
++ // Partial means, if you run out of file before you run
++ // out of pattern, then that's fine, as long as all
++ // the parts match.
++ matchOne(file, pattern, partial = false) {
++ const options = this.options;
++ // UNC paths like //?/X:/... can match X:/... and vice versa
++ // Drive letters in absolute drive or unc paths are always compared
++ // case-insensitively.
++ if (this.isWindows) {
++ const fileDrive = typeof file[0] === 'string' && /^[a-z]:$/i.test(file[0]);
++ const fileUNC = !fileDrive &&
++ file[0] === '' &&
++ file[1] === '' &&
++ file[2] === '?' &&
++ /^[a-z]:$/i.test(file[3]);
++ const patternDrive = typeof pattern[0] === 'string' && /^[a-z]:$/i.test(pattern[0]);
++ const patternUNC = !patternDrive &&
++ pattern[0] === '' &&
++ pattern[1] === '' &&
++ pattern[2] === '?' &&
++ typeof pattern[3] === 'string' &&
++ /^[a-z]:$/i.test(pattern[3]);
++ const fdi = fileUNC ? 3 : fileDrive ? 0 : undefined;
++ const pdi = patternUNC ? 3 : patternDrive ? 0 : undefined;
++ if (typeof fdi === 'number' && typeof pdi === 'number') {
++ const [fd, pd] = [file[fdi], pattern[pdi]];
++ if (fd.toLowerCase() === pd.toLowerCase()) {
++ pattern[pdi] = fd;
++ if (pdi > fdi) {
++ pattern = pattern.slice(pdi);
++ }
++ else if (fdi > pdi) {
++ file = file.slice(fdi);
++ }
++ }
++ }
++ }
++ // resolve and reduce . and .. portions in the file as well.
++ // dont' need to do the second phase, because it's only one string[]
++ const { optimizationLevel = 1 } = this.options;
++ if (optimizationLevel >= 2) {
++ file = this.levelTwoFileOptimize(file);
++ }
++ this.debug('matchOne', this, { file, pattern });
++ this.debug('matchOne', file.length, pattern.length);
++ for (var fi = 0, pi = 0, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) {
++ this.debug('matchOne loop');
++ var p = pattern[pi];
++ var f = file[fi];
++ this.debug(pattern, p, f);
++ // should be impossible.
++ // some invalid regexp stuff in the set.
++ /* c8 ignore start */
++ if (p === false) {
++ return false;
++ }
++ /* c8 ignore stop */
++ if (p === exports.GLOBSTAR) {
++ this.debug('GLOBSTAR', [pattern, p, f]);
++ // "**"
++ // a/**/b/**/c would match the following:
++ // a/b/x/y/z/c
++ // a/x/y/z/b/c
++ // a/b/x/b/x/c
++ // a/b/c
++ // To do this, take the rest of the pattern after
++ // the **, and see if it would match the file remainder.
++ // If so, return success.
++ // If not, the ** "swallows" a segment, and try again.
++ // This is recursively awful.
++ //
++ // a/**/b/**/c matching a/b/x/y/z/c
++ // - a matches a
++ // - doublestar
++ // - matchOne(b/x/y/z/c, b/**/c)
++ // - b matches b
++ // - doublestar
++ // - matchOne(x/y/z/c, c) -> no
++ // - matchOne(y/z/c, c) -> no
++ // - matchOne(z/c, c) -> no
++ // - matchOne(c, c) yes, hit
++ var fr = fi;
++ var pr = pi + 1;
++ if (pr === pl) {
++ this.debug('** at the end');
++ // a ** at the end will just swallow the rest.
++ // We have found a match.
++ // however, it will not swallow /.x, unless
++ // options.dot is set.
++ // . and .. are *never* matched by **, for explosively
++ // exponential reasons.
++ for (; fi < fl; fi++) {
++ if (file[fi] === '.' ||
++ file[fi] === '..' ||
++ (!options.dot && file[fi].charAt(0) === '.'))
++ return false;
++ }
++ return true;
++ }
++ // ok, let's see if we can swallow whatever we can.
++ while (fr < fl) {
++ var swallowee = file[fr];
++ this.debug('\nglobstar while', file, fr, pattern, pr, swallowee);
++ // XXX remove this slice. Just pass the start index.
++ if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
++ this.debug('globstar found match!', fr, fl, swallowee);
++ // found a match.
++ return true;
++ }
++ else {
++ // can't swallow "." or ".." ever.
++ // can only swallow ".foo" when explicitly asked.
++ if (swallowee === '.' ||
++ swallowee === '..' ||
++ (!options.dot && swallowee.charAt(0) === '.')) {
++ this.debug('dot detected!', file, fr, pattern, pr);
++ break;
++ }
++ // ** swallows a segment, and continue.
++ this.debug('globstar swallow a segment, and continue');
++ fr++;
++ }
++ }
++ // no match was found.
++ // However, in partial mode, we can't say this is necessarily over.
++ /* c8 ignore start */
++ if (partial) {
++ // ran out of file
++ this.debug('\n>>> no match, partial?', file, fr, pattern, pr);
++ if (fr === fl) {
++ return true;
++ }
++ }
++ /* c8 ignore stop */
++ return false;
++ }
++ // something other than **
++ // non-magic patterns just have to match exactly
++ // patterns with magic have been turned into regexps.
++ let hit;
++ if (typeof p === 'string') {
++ hit = f === p;
++ this.debug('string match', p, f, hit);
++ }
++ else {
++ hit = p.test(f);
++ this.debug('pattern match', p, f, hit);
++ }
++ if (!hit)
++ return false;
++ }
++ // Note: ending in / means that we'll get a final ""
++ // at the end of the pattern. This can only match a
++ // corresponding "" at the end of the file.
++ // If the file ends in /, then it can only match a
++ // a pattern that ends in /, unless the pattern just
++ // doesn't have any more for it. But, a/b/ should *not*
++ // match "a/b/*", even though "" matches against the
++ // [^/]*? pattern, except in partial mode, where it might
++ // simply not be reached yet.
++ // However, a/b/ should still satisfy a/*
++ // now either we fell off the end of the pattern, or we're done.
++ if (fi === fl && pi === pl) {
++ // ran out of pattern and filename at the same time.
++ // an exact hit!
++ return true;
++ }
++ else if (fi === fl) {
++ // ran out of file, but still had pattern left.
++ // this is ok if we're doing the match as part of
++ // a glob fs traversal.
++ return partial;
++ }
++ else if (pi === pl) {
++ // ran out of pattern, still have file left.
++ // this is only acceptable if we're on the very last
++ // empty segment of a file with a trailing slash.
++ // a/* should match a/b/
++ return fi === fl - 1 && file[fi] === '';
++ /* c8 ignore start */
++ }
++ else {
++ // should be unreachable.
++ throw new Error('wtf?');
++ }
++ /* c8 ignore stop */
++ }
++ braceExpand() {
++ return (0, exports.braceExpand)(this.pattern, this.options);
++ }
++ parse(pattern) {
++ (0, assert_valid_pattern_js_1.assertValidPattern)(pattern);
++ const options = this.options;
++ // shortcuts
++ if (pattern === '**')
++ return exports.GLOBSTAR;
++ if (pattern === '')
++ return '';
++ // far and away, the most common glob pattern parts are
++ // *, *.*, and *. Add a fast check method for those.
++ let m;
++ let fastTest = null;
++ if ((m = pattern.match(starRE))) {
++ fastTest = options.dot ? starTestDot : starTest;
++ }
++ else if ((m = pattern.match(starDotExtRE))) {
++ fastTest = (options.nocase
++ ? options.dot
++ ? starDotExtTestNocaseDot
++ : starDotExtTestNocase
++ : options.dot
++ ? starDotExtTestDot
++ : starDotExtTest)(m[1]);
++ }
++ else if ((m = pattern.match(qmarksRE))) {
++ fastTest = (options.nocase
++ ? options.dot
++ ? qmarksTestNocaseDot
++ : qmarksTestNocase
++ : options.dot
++ ? qmarksTestDot
++ : qmarksTest)(m);
++ }
++ else if ((m = pattern.match(starDotStarRE))) {
++ fastTest = options.dot ? starDotStarTestDot : starDotStarTest;
++ }
++ else if ((m = pattern.match(dotStarRE))) {
++ fastTest = dotStarTest;
++ }
++ const re = ast_js_1.AST.fromGlob(pattern, this.options).toMMPattern();
++ if (fastTest && typeof re === 'object') {
++ // Avoids overriding in frozen environments
++ Reflect.defineProperty(re, 'test', { value: fastTest });
++ }
++ return re;
++ }
++ makeRe() {
++ if (this.regexp || this.regexp === false)
++ return this.regexp;
++ // at this point, this.set is a 2d array of partial
++ // pattern strings, or "**".
++ //
++ // It's better to use .match(). This function shouldn't
++ // be used, really, but it's pretty convenient sometimes,
++ // when you just want to work with a regex.
++ const set = this.set;
++ if (!set.length) {
++ this.regexp = false;
++ return this.regexp;
++ }
++ const options = this.options;
++ const twoStar = options.noglobstar
++ ? star
++ : options.dot
++ ? twoStarDot
++ : twoStarNoDot;
++ const flags = new Set(options.nocase ? ['i'] : []);
++ // regexpify non-globstar patterns
++ // if ** is only item, then we just do one twoStar
++ // if ** is first, and there are more, prepend (\/|twoStar\/)? to next
++ // if ** is last, append (\/twoStar|) to previous
++ // if ** is in the middle, append (\/|\/twoStar\/) to previous
++ // then filter out GLOBSTAR symbols
++ let re = set
++ .map(pattern => {
++ const pp = pattern.map(p => {
++ if (p instanceof RegExp) {
++ for (const f of p.flags.split(''))
++ flags.add(f);
++ }
++ return typeof p === 'string'
++ ? regExpEscape(p)
++ : p === exports.GLOBSTAR
++ ? exports.GLOBSTAR
++ : p._src;
++ });
++ pp.forEach((p, i) => {
++ const next = pp[i + 1];
++ const prev = pp[i - 1];
++ if (p !== exports.GLOBSTAR || prev === exports.GLOBSTAR) {
++ return;
++ }
++ if (prev === undefined) {
++ if (next !== undefined && next !== exports.GLOBSTAR) {
++ pp[i + 1] = '(?:\\/|' + twoStar + '\\/)?' + next;
++ }
++ else {
++ pp[i] = twoStar;
++ }
++ }
++ else if (next === undefined) {
++ pp[i - 1] = prev + '(?:\\/|' + twoStar + ')?';
++ }
++ else if (next !== exports.GLOBSTAR) {
++ pp[i - 1] = prev + '(?:\\/|\\/' + twoStar + '\\/)' + next;
++ pp[i + 1] = exports.GLOBSTAR;
++ }
++ });
++ return pp.filter(p => p !== exports.GLOBSTAR).join('/');
++ })
++ .join('|');
++ // need to wrap in parens if we had more than one thing with |,
++ // otherwise only the first will be anchored to ^ and the last to $
++ const [open, close] = set.length > 1 ? ['(?:', ')'] : ['', ''];
++ // must match entire pattern
++ // ending in a * or ** will make it less strict.
++ re = '^' + open + re + close + '$';
++ // can match anything, as long as it's not this.
++ if (this.negate)
++ re = '^(?!' + re + ').+$';
++ try {
++ this.regexp = new RegExp(re, [...flags].join(''));
++ /* c8 ignore start */
++ }
++ catch (ex) {
++ // should be impossible
++ this.regexp = false;
++ }
++ /* c8 ignore stop */
++ return this.regexp;
++ }
++ slashSplit(p) {
++ // if p starts with // on windows, we preserve that
++ // so that UNC paths aren't broken. Otherwise, any number of
++ // / characters are coalesced into one, unless
++ // preserveMultipleSlashes is set to true.
++ if (this.preserveMultipleSlashes) {
++ return p.split('/');
++ }
++ else if (this.isWindows && /^\/\/[^\/]+/.test(p)) {
++ // add an extra '' for the one we lose
++ return ['', ...p.split(/\/+/)];
++ }
++ else {
++ return p.split(/\/+/);
++ }
++ }
++ match(f, partial = this.partial) {
++ this.debug('match', f, this.pattern);
++ // short-circuit in the case of busted things.
++ // comments, etc.
++ if (this.comment) {
++ return false;
++ }
++ if (this.empty) {
++ return f === '';
++ }
++ if (f === '/' && partial) {
++ return true;
++ }
++ const options = this.options;
++ // windows: need to use /, not \
++ if (this.isWindows) {
++ f = f.split('\\').join('/');
++ }
++ // treat the test path as a set of pathparts.
++ const ff = this.slashSplit(f);
++ this.debug(this.pattern, 'split', ff);
++ // just ONE of the pattern sets in this.set needs to match
++ // in order for it to be valid. If negating, then just one
++ // match means that we have failed.
++ // Either way, return on the first hit.
++ const set = this.set;
++ this.debug(this.pattern, 'set', set);
++ // Find the basename of the path by looking for the last non-empty segment
++ let filename = ff[ff.length - 1];
++ if (!filename) {
++ for (let i = ff.length - 2; !filename && i >= 0; i--) {
++ filename = ff[i];
++ }
++ }
++ for (let i = 0; i < set.length; i++) {
++ const pattern = set[i];
++ let file = ff;
++ if (options.matchBase && pattern.length === 1) {
++ file = [filename];
++ }
++ const hit = this.matchOne(file, pattern, partial);
++ if (hit) {
++ if (options.flipNegate) {
++ return true;
++ }
++ return !this.negate;
++ }
++ }
++ // didn't get any hits. this is success if it's a negative
++ // pattern, failure otherwise.
++ if (options.flipNegate) {
++ return false;
++ }
++ return this.negate;
++ }
++ static defaults(def) {
++ return exports.minimatch.defaults(def).Minimatch;
++ }
++ }
++ exports.Minimatch = Minimatch;
++ /* c8 ignore start */
++ var ast_js_2 = ast;
++ Object.defineProperty(exports, "AST", { enumerable: true, get: function () { return ast_js_2.AST; } });
++ var escape_js_2 = _escape;
++ Object.defineProperty(exports, "escape", { enumerable: true, get: function () { return escape_js_2.escape; } });
++ var unescape_js_2 = _unescape;
++ Object.defineProperty(exports, "unescape", { enumerable: true, get: function () { return unescape_js_2.unescape; } });
++ /* c8 ignore stop */
++ exports.minimatch.AST = ast_js_1.AST;
++ exports.minimatch.Minimatch = Minimatch;
++ exports.minimatch.escape = escape_js_1.escape;
++ exports.minimatch.unescape = unescape_js_1.unescape;
++
++} (commonjs));
++
++const fs = require$$0;
++const path = path$1;
++const EE = require$$2.EventEmitter;
++const Minimatch = commonjs.Minimatch;
++
++class Walker extends EE {
++ constructor (opts) {
++ opts = opts || {};
++ super(opts);
++ // set to true if this.path is a symlink, whether follow is true or not
++ this.isSymbolicLink = opts.isSymbolicLink;
++ this.path = opts.path || process.cwd();
++ this.basename = path.basename(this.path);
++ this.ignoreFiles = opts.ignoreFiles || ['.ignore'];
++ this.ignoreRules = {};
++ this.parent = opts.parent || null;
++ this.includeEmpty = !!opts.includeEmpty;
++ this.root = this.parent ? this.parent.root : this.path;
++ this.follow = !!opts.follow;
++ this.result = this.parent ? this.parent.result : new Set();
++ this.entries = null;
++ this.sawError = false;
++ this.exact = opts.exact;
++ }
++
++ sort (a, b) {
++ return a.localeCompare(b, 'en')
++ }
++
++ emit (ev, data) {
++ let ret = false;
++ if (!(this.sawError && ev === 'error')) {
++ if (ev === 'error') {
++ this.sawError = true;
++ } else if (ev === 'done' && !this.parent) {
++ data = Array.from(data)
++ .map(e => /^@/.test(e) ? `./${e}` : e).sort(this.sort);
++ this.result = data;
++ }
++
++ if (ev === 'error' && this.parent) {
++ ret = this.parent.emit('error', data);
++ } else {
++ ret = super.emit(ev, data);
++ }
++ }
++ return ret
++ }
++
++ start () {
++ fs.readdir(this.path, (er, entries) =>
++ er ? this.emit('error', er) : this.onReaddir(entries));
++ return this
++ }
++
++ isIgnoreFile (e) {
++ return e !== '.' &&
++ e !== '..' &&
++ this.ignoreFiles.indexOf(e) !== -1
++ }
++
++ onReaddir (entries) {
++ this.entries = entries;
++ if (entries.length === 0) {
++ if (this.includeEmpty) {
++ this.result.add(this.path.slice(this.root.length + 1));
++ }
++ this.emit('done', this.result);
++ } else {
++ const hasIg = this.entries.some(e =>
++ this.isIgnoreFile(e));
++
++ if (hasIg) {
++ this.addIgnoreFiles();
++ } else {
++ this.filterEntries();
++ }
++ }
++ }
++
++ addIgnoreFiles () {
++ const newIg = this.entries
++ .filter(e => this.isIgnoreFile(e));
++
++ let igCount = newIg.length;
++ const then = () => {
++ if (--igCount === 0) {
++ this.filterEntries();
++ }
++ };
++
++ newIg.forEach(e => this.addIgnoreFile(e, then));
++ }
++
++ addIgnoreFile (file, then) {
++ const ig = path.resolve(this.path, file);
++ fs.readFile(ig, 'utf8', (er, data) =>
++ er ? this.emit('error', er) : this.onReadIgnoreFile(file, data, then));
++ }
++
++ onReadIgnoreFile (file, data, then) {
++ const mmopt = {
++ matchBase: true,
++ dot: true,
++ flipNegate: true,
++ nocase: true,
++ };
++ const rules = data.split(/\r?\n/)
++ .filter(line => !/^#|^$/.test(line.trim()))
++ .map(rule => {
++ return new Minimatch(rule.trim(), mmopt)
++ });
++
++ this.ignoreRules[file] = rules;
++
++ then();
++ }
++
++ filterEntries () {
++ // at this point we either have ignore rules, or just inheriting
++ // this exclusion is at the point where we know the list of
++ // entries in the dir, but don't know what they are. since
++ // some of them *might* be directories, we have to run the
++ // match in dir-mode as well, so that we'll pick up partials
++ // of files that will be included later. Anything included
++ // at this point will be checked again later once we know
++ // what it is.
++ const filtered = this.entries.map(entry => {
++ // at this point, we don't know if it's a dir or not.
++ const passFile = this.filterEntry(entry);
++ const passDir = this.filterEntry(entry, true);
++ return (passFile || passDir) ? [entry, passFile, passDir] : false
++ }).filter(e => e);
++
++ // now we stat them all
++ // if it's a dir, and passes as a dir, then recurse
++ // if it's not a dir, but passes as a file, add to set
++ let entryCount = filtered.length;
++ if (entryCount === 0) {
++ this.emit('done', this.result);
++ } else {
++ const then = () => {
++ if (--entryCount === 0) {
++ this.emit('done', this.result);
++ }
++ };
++ filtered.forEach(filt => {
++ const entry = filt[0];
++ const file = filt[1];
++ const dir = filt[2];
++ this.stat({ entry, file, dir }, then);
++ });
++ }
++ }
++
++ onstat ({ st, entry, file, dir, isSymbolicLink }, then) {
++ const abs = this.path + '/' + entry;
++ if (!st.isDirectory()) {
++ if (file) {
++ this.result.add(abs.slice(this.root.length + 1));
++ }
++ then();
++ } else {
++ // is a directory
++ if (dir) {
++ this.walker(entry, { isSymbolicLink, exact: file || this.filterEntry(entry + '/') }, then);
++ } else {
++ then();
++ }
++ }
++ }
++
++ stat ({ entry, file, dir }, then) {
++ const abs = this.path + '/' + entry;
++ fs.lstat(abs, (lstatErr, lstatResult) => {
++ if (lstatErr) {
++ this.emit('error', lstatErr);
++ } else {
++ const isSymbolicLink = lstatResult.isSymbolicLink();
++ if (this.follow && isSymbolicLink) {
++ fs.stat(abs, (statErr, statResult) => {
++ if (statErr) {
++ this.emit('error', statErr);
++ } else {
++ this.onstat({ st: statResult, entry, file, dir, isSymbolicLink }, then);
++ }
++ });
++ } else {
++ this.onstat({ st: lstatResult, entry, file, dir, isSymbolicLink }, then);
++ }
++ }
++ });
++ }
++
++ walkerOpt (entry, opts) {
++ return {
++ path: this.path + '/' + entry,
++ parent: this,
++ ignoreFiles: this.ignoreFiles,
++ follow: this.follow,
++ includeEmpty: this.includeEmpty,
++ ...opts,
++ }
++ }
++
++ walker (entry, opts, then) {
++ new Walker(this.walkerOpt(entry, opts)).on('done', then).start();
++ }
++
++ filterEntry (entry, partial, entryBasename) {
++ let included = true;
++
++ // this = /a/b/c
++ // entry = d
++ // parent /a/b sees c/d
++ if (this.parent && this.parent.filterEntry) {
++ const parentEntry = this.basename + '/' + entry;
++ const parentBasename = entryBasename || entry;
++ included = this.parent.filterEntry(parentEntry, partial, parentBasename);
++ if (!included && !this.exact) {
++ return false
++ }
++ }
++
++ this.ignoreFiles.forEach(f => {
++ if (this.ignoreRules[f]) {
++ this.ignoreRules[f].forEach(rule => {
++ // negation means inclusion
++ // so if it's negated, and already included, no need to check
++ // likewise if it's neither negated nor included
++ if (rule.negate !== included) {
++ const isRelativeRule = entryBasename && rule.globParts.some(part =>
++ part.length <= (part.slice(-1)[0] ? 1 : 2)
++ );
++
++ // first, match against /foo/bar
++ // then, against foo/bar
++ // then, in the case of partials, match with a /
++ // then, if also the rule is relative, match against basename
++ const match = rule.match('/' + entry) ||
++ rule.match(entry) ||
++ !!partial && (
++ rule.match('/' + entry + '/') ||
++ rule.match(entry + '/') ||
++ rule.negate && (
++ rule.match('/' + entry, true) ||
++ rule.match(entry, true)) ||
++ isRelativeRule && (
++ rule.match('/' + entryBasename + '/') ||
++ rule.match(entryBasename + '/') ||
++ rule.negate && (
++ rule.match('/' + entryBasename, true) ||
++ rule.match(entryBasename, true))));
++
++ if (match) {
++ included = rule.negate;
++ }
++ }
++ });
++ }
++ });
++
++ return included
++ }
++}
++
++class WalkerSync extends Walker {
++ start () {
++ this.onReaddir(fs.readdirSync(this.path));
++ return this
++ }
++
++ addIgnoreFile (file, then) {
++ const ig = path.resolve(this.path, file);
++ this.onReadIgnoreFile(file, fs.readFileSync(ig, 'utf8'), then);
++ }
++
++ stat ({ entry, file, dir }, then) {
++ const abs = this.path + '/' + entry;
++ let st = fs.lstatSync(abs);
++ const isSymbolicLink = st.isSymbolicLink();
++ if (this.follow && isSymbolicLink) {
++ st = fs.statSync(abs);
++ }
++
++ // console.error('STAT SYNC', {st, entry, file, dir, isSymbolicLink, then})
++ this.onstat({ st, entry, file, dir, isSymbolicLink }, then);
++ }
++
++ walker (entry, opts, then) {
++ new WalkerSync(this.walkerOpt(entry, opts)).start();
++ then();
++ }
++}
++
++const walk$1 = (opts, callback) => {
++ const p = new Promise((resolve, reject) => {
++ new Walker(opts).on('done', resolve).on('error', reject).start();
++ });
++ return callback ? p.then(res => callback(null, res), callback) : p
++};
++
++const walkSync = opts => new WalkerSync(opts).start().result;
++
++var lib$1 = walk$1;
++walk$1.sync = walkSync;
++walk$1.Walker = Walker;
++walk$1.WalkerSync = WalkerSync;
++
++const { Walker: IgnoreWalker } = lib$1;
++const { lstatSync: lstat, readFileSync: readFile } = require$$0;
++const { basename, dirname, extname, join, relative, resolve, sep } = path$1;
++
++// symbols used to represent synthetic rule sets
++const defaultRules = Symbol('npm-packlist.rules.default');
++const strictRules = Symbol('npm-packlist.rules.strict');
++
++// There may be others, but :?|<> are handled by node-tar
++const nameIsBadForWindows = file => /\*/.test(file);
++
++// these are the default rules that are applied to everything except for non-link bundled deps
++const defaults = [
++ '.npmignore',
++ '.gitignore',
++ '**/.git',
++ '**/.svn',
++ '**/.hg',
++ '**/CVS',
++ '**/.git/**',
++ '**/.svn/**',
++ '**/.hg/**',
++ '**/CVS/**',
++ '/.lock-wscript',
++ '/.wafpickle-*',
++ '/build/config.gypi',
++ 'npm-debug.log',
++ '**/.npmrc',
++ '.*.swp',
++ '.DS_Store',
++ '**/.DS_Store/**',
++ '._*',
++ '**/._*/**',
++ '*.orig',
++ '/archived-packages/**',
++];
++
++const strictDefaults = [
++ // these are forcibly excluded
++ '/.git',
++];
++
++const normalizePath = (path) => path.split('\\').join('/');
++
++const readOutOfTreeIgnoreFiles = (root, rel, result = []) => {
++ for (const file of ['.npmignore', '.gitignore']) {
++ try {
++ const ignoreContent = readFile(join(root, file), { encoding: 'utf8' });
++ result.push(ignoreContent);
++ // break the loop immediately after reading, this allows us to prioritize
++ // the .npmignore and discard the .gitignore if one is present
++ break
++ } catch (err) {
++ // we ignore ENOENT errors completely because we don't care if the file doesn't exist
++ // but we throw everything else because failing to read a file that does exist is
++ // something that the user likely wants to know about
++ // istanbul ignore next -- we do not need to test a thrown error
++ if (err.code !== 'ENOENT') {
++ throw err
++ }
++ }
++ }
++
++ if (!rel) {
++ return result
++ }
++
++ const firstRel = rel.split(sep, 1)[0];
++ const newRoot = join(root, firstRel);
++ const newRel = relative(newRoot, join(root, rel));
++
++ return readOutOfTreeIgnoreFiles(newRoot, newRel, result)
++};
++
++class PackWalker extends IgnoreWalker {
++ constructor (tree, opts) {
++ const options = {
++ ...opts,
++ includeEmpty: false,
++ follow: false,
++ // we path.resolve() here because ignore-walk doesn't do it and we want full paths
++ path: resolve(opts?.path || tree.path).replace(/\\/g, '/'),
++ ignoreFiles: opts?.ignoreFiles || [
++ defaultRules,
++ 'package.json',
++ '.npmignore',
++ '.gitignore',
++ strictRules,
++ ],
++ };
++
++ super(options);
++ this.isPackage = options.isPackage;
++ this.seen = options.seen || new Set();
++ this.tree = tree;
++ this.requiredFiles = options.requiredFiles || [];
++
++ const additionalDefaults = [];
++ if (options.prefix && options.workspaces) {
++ const path = normalizePath(options.path);
++ const prefix = normalizePath(options.prefix);
++ const workspaces = options.workspaces.map((ws) => normalizePath(ws));
++
++ // istanbul ignore else - this does nothing unless we need it to
++ if (path !== prefix && workspaces.includes(path)) {
++ // if path and prefix are not the same directory, and workspaces has path in it
++ // then we know path is a workspace directory. in order to not drop ignore rules
++ // from directories between the workspaces root (prefix) and the workspace itself
++ // (path) we need to find and read those now
++ const relpath = relative(options.prefix, dirname(options.path));
++ additionalDefaults.push(...readOutOfTreeIgnoreFiles(options.prefix, relpath));
++ } else if (path === prefix) {
++ // on the other hand, if the path and prefix are the same, then we ignore workspaces
++ // so that we don't pack a workspace as part of the root project. append them as
++ // normalized relative paths from the root
++ additionalDefaults.push(...workspaces.map((w) => normalizePath(relative(options.path, w))));
++ }
++ }
++
++ // go ahead and inject the default rules now
++ this.injectRules(defaultRules, [...defaults, ...additionalDefaults]);
++
++ if (!this.isPackage) {
++ // if this instance is not a package, then place some strict default rules, and append
++ // known required files for this directory
++ this.injectRules(strictRules, [
++ ...strictDefaults,
++ ...this.requiredFiles.map((file) => `!${file}`),
++ ]);
++ }
++ }
++
++ // overridden method: we intercept the reading of the package.json file here so that we can
++ // process it into both the package.json file rules as well as the strictRules synthetic rule set
++ addIgnoreFile (file, callback) {
++ // if we're adding anything other than package.json, then let ignore-walk handle it
++ if (file !== 'package.json' || !this.isPackage) {
++ return super.addIgnoreFile(file, callback)
++ }
++
++ return this.processPackage(callback)
++ }
++
++ // overridden method: if we're done, but we're a package, then we also need to evaluate bundles
++ // before we actually emit our done event
++ emit (ev, data) {
++ if (ev !== 'done' || !this.isPackage) {
++ return super.emit(ev, data)
++ }
++
++ // we intentionally delay the done event while keeping the function sync here
++ // eslint-disable-next-line promise/catch-or-return, promise/always-return
++ this.gatherBundles().then(() => {
++ super.emit('done', this.result);
++ });
++ return true
++ }
++
++ // overridden method: before actually filtering, we make sure that we've removed the rules for
++ // files that should no longer take effect due to our order of precedence
++ filterEntries () {
++ if (this.ignoreRules['package.json']) {
++ // package.json means no .npmignore or .gitignore
++ this.ignoreRules['.npmignore'] = null;
++ this.ignoreRules['.gitignore'] = null;
++ } else if (this.ignoreRules['.npmignore']) {
++ // .npmignore means no .gitignore
++ this.ignoreRules['.gitignore'] = null;
++ }
++
++ return super.filterEntries()
++ }
++
++ // overridden method: we never want to include anything that isn't a file or directory
++ onstat (opts, callback) {
++ if (!opts.st.isFile() && !opts.st.isDirectory()) {
++ return callback()
++ }
++
++ return super.onstat(opts, callback)
++ }
++
++ // overridden method: we want to refuse to pack files that are invalid, node-tar protects us from
++ // a lot of them but not all
++ stat (opts, callback) {
++ if (nameIsBadForWindows(opts.entry)) {
++ return callback()
++ }
++
++ return super.stat(opts, callback)
++ }
++
++ // overridden method: this is called to create options for a child walker when we step
++ // in to a normal child directory (this will never be a bundle). the default method here
++ // copies the root's `ignoreFiles` value, but we don't want to respect package.json for
++ // subdirectories, so we override it with a list that intentionally omits package.json
++ walkerOpt (entry, opts) {
++ let ignoreFiles = null;
++
++ // however, if we have a tree, and we have workspaces, and the directory we're about
++ // to step into is a workspace, then we _do_ want to respect its package.json
++ if (this.tree.workspaces) {
++ const workspaceDirs = [...this.tree.workspaces.values()]
++ .map((dir) => dir.replace(/\\/g, '/'));
++
++ const entryPath = join(this.path, entry).replace(/\\/g, '/');
++ if (workspaceDirs.includes(entryPath)) {
++ ignoreFiles = [
++ defaultRules,
++ 'package.json',
++ '.npmignore',
++ '.gitignore',
++ strictRules,
++ ];
++ }
++ } else {
++ ignoreFiles = [
++ defaultRules,
++ '.npmignore',
++ '.gitignore',
++ strictRules,
++ ];
++ }
++
++ return {
++ ...super.walkerOpt(entry, opts),
++ ignoreFiles,
++ // we map over our own requiredFiles and pass ones that are within this entry
++ requiredFiles: this.requiredFiles
++ .map((file) => {
++ if (relative(file, entry) === '..') {
++ return relative(entry, file).replace(/\\/g, '/')
++ }
++ return false
++ })
++ .filter(Boolean),
++ }
++ }
++
++ // overridden method: we want child walkers to be instances of this class, not ignore-walk
++ walker (entry, opts, callback) {
++ new PackWalker(this.tree, this.walkerOpt(entry, opts)).on('done', callback).start();
++ }
++
++ // overridden method: we use a custom sort method to help compressibility
++ sort (a, b) {
++ // optimize for compressibility
++ // extname, then basename, then locale alphabetically
++ // https://twitter.com/isntitvacant/status/1131094910923231232
++ const exta = extname(a).toLowerCase();
++ const extb = extname(b).toLowerCase();
++ const basea = basename(a).toLowerCase();
++ const baseb = basename(b).toLowerCase();
++
++ return exta.localeCompare(extb, 'en') ||
++ basea.localeCompare(baseb, 'en') ||
++ a.localeCompare(b, 'en')
++ }
++
++ // convenience method: this joins the given rules with newlines, appends a trailing newline,
++ // and calls the internal onReadIgnoreFile method
++ injectRules (filename, rules, callback = () => {}) {
++ this.onReadIgnoreFile(filename, `${rules.join('\n')}\n`, callback);
++ }
++
++ // custom method: this is called by addIgnoreFile when we find a package.json, it uses the
++ // arborist tree to pull both default rules and strict rules for the package
++ processPackage (callback) {
++ const {
++ bin,
++ browser,
++ files,
++ main,
++ } = this.tree.package;
++
++ // rules in these arrays are inverted since they are patterns we want to _not_ ignore
++ const ignores = [];
++ const strict = [
++ ...strictDefaults,
++ '!/package.json',
++ '!/readme{,.*[^~$]}',
++ '!/copying{,.*[^~$]}',
++ '!/license{,.*[^~$]}',
++ '!/licence{,.*[^~$]}',
++ '/.git',
++ '/node_modules',
++ '.npmrc',
++ '/package-lock.json',
++ '/yarn.lock',
++ '/pnpm-lock.yaml',
++ ];
++
++ // if we have a files array in our package, we need to pull rules from it
++ if (files) {
++ for (let file of files) {
++ // invert the rule because these are things we want to include
++ if (file.startsWith('./')) {
++ file = file.slice(1);
++ }
++ if (file.endsWith('/*')) {
++ file += '*';
++ }
++ const inverse = `!${file}`;
++ try {
++ // if an entry in the files array is a specific file, then we need to include it as a
++ // strict requirement for this package. if it's a directory or a pattern, it's a default
++ // pattern instead. this is ugly, but we have to stat to find out if it's a file
++ const stat = lstat(join(this.path, file.replace(/^!+/, '')).replace(/\\/g, '/'));
++ // if we have a file and we know that, it's strictly required
++ if (stat.isFile()) {
++ strict.unshift(inverse);
++ this.requiredFiles.push(file.startsWith('/') ? file.slice(1) : file);
++ } else if (stat.isDirectory()) {
++ // otherwise, it's a default ignore, and since we got here we know it's not a pattern
++ // so we include the directory contents
++ ignores.push(inverse);
++ ignores.push(`${inverse}/**`);
++ }
++ // if the thing exists, but is neither a file or a directory, we don't want it at all
++ } catch (err) {
++ // if lstat throws, then we assume we're looking at a pattern and treat it as a default
++ ignores.push(inverse);
++ }
++ }
++
++ // we prepend a '*' to exclude everything, followed by our inverted file rules
++ // which now mean to include those
++ this.injectRules('package.json', ['*', ...ignores]);
++ }
++
++ // browser is required
++ if (browser) {
++ strict.push(`!/${browser}`);
++ }
++
++ // main is required
++ if (main) {
++ strict.push(`!/${main}`);
++ }
++
++ // each bin is required
++ if (bin) {
++ for (const key in bin) {
++ strict.push(`!/${bin[key]}`);
++ }
++ }
++
++ // and now we add all of the strict rules to our synthetic file
++ this.injectRules(strictRules, strict, callback);
++ }
++
++ // custom method: after we've finished gathering the files for the root package, we call this
++ // before emitting the 'done' event in order to gather all of the files for bundled deps
++ async gatherBundles () {
++ if (this.seen.has(this.tree)) {
++ return
++ }
++
++ // add this node to our seen tracker
++ this.seen.add(this.tree);
++
++ // if we're the project root, then we look at our bundleDependencies, otherwise we got here
++ // because we're a bundled dependency of the root, which means we need to include all prod
++ // and optional dependencies in the bundle
++ let toBundle;
++ if (this.tree.isProjectRoot) {
++ const { bundleDependencies } = this.tree.package;
++ toBundle = bundleDependencies || [];
++ } else {
++ const { dependencies, optionalDependencies } = this.tree.package;
++ toBundle = Object.keys(dependencies || {}).concat(Object.keys(optionalDependencies || {}));
++ }
++
++ for (const dep of toBundle) {
++ const edge = this.tree.edgesOut.get(dep);
++ // no edgeOut = missing node, so skip it. we can't pack it if it's not here
++ // we also refuse to pack peer dependencies and dev dependencies
++ if (!edge || edge.peer || edge.dev) {
++ continue
++ }
++
++ // get a reference to the node we're bundling
++ const node = this.tree.edgesOut.get(dep).to;
++ // if there's no node, this is most likely an optional dependency that hasn't been
++ // installed. just skip it.
++ if (!node) {
++ continue
++ }
++ // we use node.path for the path because we want the location the node was linked to,
++ // not where it actually lives on disk
++ const path = node.path;
++ // but link nodes don't have edgesOut, so we need to pass in the target of the node
++ // in order to make sure we correctly traverse its dependencies
++ const tree = node.target;
++
++ // and start building options to be passed to the walker for this package
++ const walkerOpts = {
++ path,
++ isPackage: true,
++ ignoreFiles: [],
++ seen: this.seen, // pass through seen so we can prevent infinite circular loops
++ };
++
++ // if our node is a link, we apply defaultRules. we don't do this for regular bundled
++ // deps because their .npmignore and .gitignore files are excluded by default and may
++ // override defaults
++ if (node.isLink) {
++ walkerOpts.ignoreFiles.push(defaultRules);
++ }
++
++ // _all_ nodes will follow package.json rules from their package root
++ walkerOpts.ignoreFiles.push('package.json');
++
++ // only link nodes will obey .npmignore or .gitignore
++ if (node.isLink) {
++ walkerOpts.ignoreFiles.push('.npmignore');
++ walkerOpts.ignoreFiles.push('.gitignore');
++ }
++
++ // _all_ nodes follow strict rules
++ walkerOpts.ignoreFiles.push(strictRules);
++
++ // create a walker for this dependency and gather its results
++ const walker = new PackWalker(tree, walkerOpts);
++ const bundled = await new Promise((pResolve, pReject) => {
++ walker.on('error', pReject);
++ walker.on('done', pResolve);
++ walker.start();
++ });
++
++ // now we make sure we have our paths correct from the root, and accumulate everything into
++ // our own result set to deduplicate
++ const relativeFrom = relative(this.root, walker.path);
++ for (const file of bundled) {
++ this.result.add(join(relativeFrom, file).replace(/\\/g, '/'));
++ }
++ }
++ }
++}
++
++const walk = (tree, options, callback) => {
++ if (typeof options === 'function') {
++ callback = options;
++ options = {};
++ }
++ const p = new Promise((pResolve, pReject) => {
++ new PackWalker(tree, { ...options, isPackage: true })
++ .on('done', pResolve).on('error', pReject).start();
++ });
++ return callback ? p.then(res => callback(null, res), callback) : p
++};
++
++var lib = walk;
++walk.Walker = PackWalker;
++
++var packlist = /*@__PURE__*/getDefaultExportFromCjs(lib);
++
++const edgesOut = /* @__PURE__ */ new Map();
++const getNpmPacklist = (absoluteLinkPackagePath, packageJson) => packlist({
++ path: absoluteLinkPackagePath,
++ package: packageJson,
++ // @ts-expect-error outdated types
++ edgesOut
++});
++
++const cwd = process.cwd();
++const cwdPath = (filePath) => path$2.relative(cwd, filePath);
++
++const getPrettyTime = () => (/* @__PURE__ */ new Date()).toLocaleTimeString(
++ void 0,
++ {
++ hour: "numeric",
++ minute: "numeric",
++ second: "numeric",
++ hour12: true
++ }
++);
++
++const waitFor = (test, interval, maxTimeout, errorMessage) => new Promise(async (resolve, reject) => {
++ const startTime = Date.now();
++ let attempts = 0;
++ const maxAttempts = Math.floor(maxTimeout / interval);
++ const attempt = async () => {
++ attempts++;
++ try {
++ const result = await test();
++ if (result) {
++ return resolve();
++ }
++ throw new Error();
++ } catch (error) {
++ const numAttemptsRemaining = maxAttempts - attempts;
++ console.error(red(` \u{1F615} Error: ${errorMessage}`), " retrying in", yellow(`${interval}ms`), ".", yellow(`${numAttemptsRemaining} attempts remaining`));
++ }
++ if (Date.now() - startTime >= maxTimeout) {
++ console.error(red(` \u{1F635} Error: ${errorMessage}. Giving up after ${maxAttempts} attempts`));
++ return reject();
++ }
++ setTimeout(attempt, interval);
++ };
++ await attempt();
++});
++
++const hardlinkPackage = async (linkPath, absoluteLinkPackagePath, packageJson, publishFilesPromise = getNpmPacklist(
++ absoluteLinkPackagePath,
++ packageJson
++), interval = 500, maxBuildTime = 3e4) => {
++ const [oldPublishFiles, publishFiles] = await Promise.all([
++ getNpmPacklist(
++ linkPath,
++ /**
++ * This is evaluated in the context of the new package.json since that
++ * defines which files belong to the package.
++ */
++ packageJson
++ ),
++ publishFilesPromise
++ ]);
++ console.log(`Linking ${magenta(packageJson.name)} in publish mode:`);
++ await Promise.all(publishFiles.map(async (file) => {
++ const sourcePath = path$2.join(absoluteLinkPackagePath, file);
++ await waitFor(
++ async () => await fsExists(sourcePath),
++ interval,
++ maxBuildTime,
++ ""
++ );
++ }));
++ await Promise.all(
++ publishFiles.map(async (file) => {
++ const sourcePath = path$2.join(absoluteLinkPackagePath, file);
++ const targetPath = path$2.join(linkPath, file);
++ await fs$2.mkdir(
++ path$2.dirname(targetPath),
++ { recursive: true }
++ );
++ try {
++ await hardlink(sourcePath, targetPath);
++ } catch (error) {
++ console.warn(
++ ` ${red("\u2716 Failed to link")}`,
++ cyan(cwdPath(targetPath)),
++ "\u2192",
++ cyan(cwdPath(sourcePath)),
++ error.message ?? error
++ );
++ return;
++ }
++ const fileIndex = oldPublishFiles.indexOf(file);
++ if (fileIndex > -1) {
++ oldPublishFiles.splice(fileIndex, 1);
++ }
++ console.log(
++ ` ${green("\u2714")}`,
++ cyan(cwdPath(targetPath)),
++ "\u2192",
++ cyan(cwdPath(sourcePath))
++ );
++ })
++ );
++ await Promise.all(
++ oldPublishFiles.map(async (file) => {
++ console.log(cyan(` \u{1F6AE} ${file} no longer in publish list, deleting it. If you did not intend to do this, something probably went wrong. See https://github.com/privatenumber/link?tab=readme-ov-file#publish-mode`));
++ await fs$2.rm(path$2.join(linkPath, file), {
++ force: true
++ });
++ })
++ );
++};
++
++const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
++
++const isValidSetup = async (linkPath, expectedPrefix) => {
++ const linkPathStat = await fs$2.stat(linkPath).catch(() => null);
++ if (!linkPathStat?.isDirectory()) {
++ return false;
++ }
++ const linkPathReal = await fs$2.realpath(linkPath);
++ return linkPathReal.startsWith(expectedPrefix);
++};
++const linkPublishMode = async (basePackagePath, linkPackagePath, watchMode, litmus, delay = 2e3, interval = 500, maxBuildTime = 3e4) => {
++ const absoluteLinkPackagePath = path$2.resolve(basePackagePath, linkPackagePath);
++ const packageJson = await readPackageJson(absoluteLinkPackagePath);
++ const expectedPrefix = path$2.join(basePackagePath, "node_modules/");
++ const linkPath = path$2.join(expectedPrefix, packageJson.name);
++ if (!await isValidSetup(linkPath, expectedPrefix)) {
++ console.error(
++ defaultOutdent`
++ Error: Package ${magenta(packageJson.name)} is not set up
++
++ ${bold("Setup instructions")}
++ 1. In the Dependency package, create a tarball:
++ ${dim("$ npm pack")}
++
++ 2. In the Consuming package, install the tarball and link the Dependency:
++ ${dim("$ npm install --no-save ")}
++ ${dim("$ npx link publish ")}
++
++ 3. Start developing!
++
++ Learn more: https://npmjs.com/link
++ `
++ );
++ return;
++ }
++ const debouncedSleepForDelay = pDebounce(sleep, delay);
++ const debouncedHardlinkPackage = debounce$1(hardlinkPackage, delay);
++ await hardlinkPackage(
++ linkPath,
++ absoluteLinkPackagePath,
++ packageJson
++ );
++ if (watchMode) {
++ const globOptions = {
++ globstar: true,
++ extended: true
++ };
++ const ignoreFiles = [
++ // Files
++ "**/{npm-debug.log,*.orig,package-lock.json,yarn.lock,pnpm-lock.yaml}",
++ // Folders
++ "**/node_modules/**",
++ // Hidden files
++ "**/.{_*,*.swp,DS_Store,gitignore,npmrc,npmignore,lock-wscript,.wafpickle-*}",
++ // Hidden folders
++ "**/.{_*,git,svn,hg,CVS}/**"
++ ].map((glob) => globToRegexp$1(glob, globOptions));
++ const watcher = fs$2.watch(
++ absoluteLinkPackagePath,
++ { recursive: true }
++ );
++ for await (const { eventType, filename } of watcher) {
++ if (!filename) {
++ continue;
++ }
++ const shouldIgnore = ignoreFiles.some((ignoreFile) => ignoreFile.test(filename));
++ if (shouldIgnore) {
++ continue;
++ }
++ await debouncedSleepForDelay(delay);
++ if (litmus) {
++ await waitFor(
++ async () => fsExists(path$2.join(absoluteLinkPackagePath, litmus)),
++ interval,
++ maxBuildTime,
++ ""
++ );
++ }
++ const publishFiles = await getNpmPacklist(
++ absoluteLinkPackagePath,
++ packageJson
++ );
++ if (!publishFiles.includes(filename)) {
++ continue;
++ }
++ console.log(`
++${dim(getPrettyTime())}`, "Detected", yellow(eventType), "in", `${cyan(cwdPath(path$2.join(absoluteLinkPackagePath, filename)))}
++`);
++ await debouncedHardlinkPackage(
++ linkPath,
++ absoluteLinkPackagePath,
++ packageJson,
++ publishFiles
++ );
++ }
++ }
++};
++
++const publishCommand = G({
++ name: "publish",
++ parameters: [""],
++ flags: {
++ watch: {
++ type: Boolean,
++ alias: "w",
++ description: "Watch for changes in the package and automatically relink"
++ },
++ litmus: {
++ type: String,
++ alias: "l",
++ description: "If using the --watch flag, look for this file in the linked package to see if it's ready to re-link"
++ },
++ delay: {
++ type: Number,
++ alias: "d",
++ description: "If using the --watch flag without the litmus flag, wait this amount of time (in ms) after detecting changes before refreshing the packlist and re-linking",
++ default: 2e3
++ },
++ interval: {
++ type: Number,
++ alias: "i",
++ description: "If using the --watch flag, poll for completed builds at this frequency (in ms)",
++ default: 500
++ },
++ maxBuildTime: {
++ type: Number,
++ alias: "m",
++ description: "If using the --watch flag, the maximum amount of time to wait for all expected files to appear before re-linking",
++ default: 3e4
++ }
++ },
++ help: {
++ description: "Link a package to simulate an environment similar to `npm install`"
++ }
++});
++const publishHandler = async (cwdProjectPath, packagePaths, flags) => {
++ if (packagePaths.length > 0) {
++ await Promise.all(
++ packagePaths.map(
++ (linkPackagePath) => linkPublishMode(
++ cwdProjectPath,
++ linkPackagePath,
++ flags.watch,
++ flags.litmus,
++ flags.delay,
++ flags.interval,
++ flags.maxBuildTime
++ )
++ )
++ );
++ }
++};
++
++(async () => {
++ const argv = Z({
++ name: "link",
++ parameters: ["[package paths...]"],
++ flags: {
++ deep: {
++ type: Boolean,
++ alias: "d",
++ description: "Run `npx link` on dependencies if they have a link.config.json"
++ }
++ },
++ help: {
++ description: "A better `npm link` -- symlink local dependencies to the current project",
++ render: (nodes, renderers) => {
++ nodes[0].data = "npx link\n";
++ nodes.splice(2, 0, {
++ type: "section",
++ data: {
++ title: "Website",
++ body: "https://www.npmjs.com/package/link"
++ }
++ });
++ return renderers.render(nodes);
++ }
++ },
++ commands: [
++ publishCommand
++ ]
++ });
++ const cwdProjectPath = await fs$1.realpath(process.cwd());
++ if (!argv.command) {
++ const { packagePaths } = argv._;
++ if (packagePaths.length > 0) {
++ await Promise.all(
++ packagePaths.map(
++ (linkPackagePath) => linkPackage(
++ cwdProjectPath,
++ linkPackagePath,
++ argv.flags
++ )
++ )
++ );
++ return;
++ }
++ const config = await loadConfig(cwdProjectPath);
++ if (!config) {
++ console.warn(
++ defaultOutdent`
+ Warning: Config file "link.config.json" not found in current directory.
+ Read the documentation to learn more: https://www.npmjs.com/package/link
+- `),u.showHelp();return}await Vu(e,D,{deep:u.flags.deep})}})().catch(u=>{console.error("Error:",u.message),process.exit(1)});
++ `
++ );
++ argv.showHelp();
++ return;
++ }
++ await linkFromConfig(
++ cwdProjectPath,
++ config,
++ {
++ deep: argv.flags.deep
++ }
++ );
++ } else if (argv.command === "publish") {
++ await publishHandler(
++ cwdProjectPath,
++ argv._,
++ argv.flags
++ );
++ }
++})().catch((error) => {
++ console.error("Error:", error.message);
++ process.exit(1);
++});
+diff --git a/node_modules/link/dist/libs/fsevents.node b/node_modules/link/dist/libs/fsevents.node
+new file mode 100755
+index 0000000..1cc3345
+Binary files /dev/null and b/node_modules/link/dist/libs/fsevents.node differ
diff --git a/src/CONST.ts b/src/CONST.ts
index db66b7e16a23..2d1396e7bfa6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -2085,6 +2085,7 @@ const CONST = {
ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled',
ARE_REPORT_FIELDS_ENABLED: 'areReportFieldsEnabled',
ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled',
+ ARE_COMPANY_CARDS_ENABLED: 'areCompanyCardsEnabled',
ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled',
ARE_INVOICES_ENABLED: 'areInvoicesEnabled',
ARE_TAXES_ENABLED: 'tax',
@@ -2428,6 +2429,7 @@ const CONST = {
WORKSPACE_MEMBERS: 'WorkspaceManageMembers',
WORKSPACE_EXPENSIFY_CARD: 'WorkspaceExpensifyCard',
WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows',
+ WORKSPACE_COMPANY_CARDS: 'WorkspaceCompanyCards',
WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount',
WORKSPACE_SETTINGS: 'WorkspaceSettings',
WORKSPACE_FEATURES: 'WorkspaceFeatures',
@@ -5479,6 +5481,14 @@ const CONST = {
description: 'workspace.upgrade.taxCodes.description' as const,
icon: 'Coins',
},
+ companyCards: {
+ id: 'companyCards' as const,
+ alias: 'company-cards',
+ name: 'Company Cards',
+ title: 'workspace.upgrade.companyCards.title' as const,
+ description: 'workspace.upgrade.companyCards.description' as const,
+ icon: 'CompanyCard',
+ },
rules: {
id: 'rules' as const,
alias: 'rules',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index b7b6cf53a176..8d60a5b57511 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -458,6 +458,7 @@ const ONYXKEYS = {
// Shared NVPs
/** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */
SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_',
+ SHARED_NVP_PRIVATE_DOMAIN_MEMBER: 'sharedNVP_private_domain_member_',
/**
* Stores the card list for a given fundID and feed in the format: cards__
@@ -749,6 +750,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress;
[ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
+ [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CompanyCards;
[ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;
[ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 73271d85ea49..47a2ad76209e 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -928,6 +928,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/expensify-card/settings/frequency',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/frequency` as const,
},
+ WORKSPACE_COMPANY_CARDS: {
+ route: 'settings/workspaces/:policyID/company-cards',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards` as const,
+ },
WORKSPACE_RULES: {
route: 'settings/workspaces/:policyID/rules',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 142b2f80a66e..686a752ad360 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -364,6 +364,7 @@ const SCREENS = {
RATE_AND_UNIT: 'Workspace_RateAndUnit',
RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate',
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
+ COMPANY_CARDS: 'Workspace_CompanyCards',
EXPENSIFY_CARD: 'Workspace_ExpensifyCard',
EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details',
EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit',
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index 28f44aabb068..782a7c2becac 100644
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -435,7 +435,6 @@ function AttachmentModal({
onSelected: () => {
setIsDeleteReceiptConfirmModalVisible(true);
},
- shouldCallAfterModalHide: true,
});
}
return menuItems;
diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx
index 8a018101b63e..a1b8524dd293 100644
--- a/src/components/AvatarWithImagePicker.tsx
+++ b/src/components/AvatarWithImagePicker.tsx
@@ -11,6 +11,7 @@ import * as FileUtils from '@libs/fileDownload/FileUtils';
import getImageResolution from '@libs/fileDownload/getImageResolution';
import type {AvatarSource} from '@libs/UserUtils';
import variables from '@styles/variables';
+import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
@@ -43,7 +44,6 @@ type MenuItem = {
icon: IconAsset;
text: string;
onSelected: () => void;
- shouldCallAfterModalHide?: boolean;
};
type AvatarWithImagePickerProps = {
@@ -260,19 +260,19 @@ function AvatarWithImagePicker({
* Create menu items list for avatar menu
*/
const createMenuItems = (openPicker: OpenPicker): MenuItem[] => {
- const menuItems: MenuItem[] = [
+ const menuItems = [
{
icon: Expensicons.Upload,
text: translate('avatarWithImagePicker.uploadPhoto'),
- onSelected: () => {
- if (Browser.isSafari()) {
- return;
- }
- openPicker({
- onPicked: showAvatarCropModal,
- });
- },
- shouldCallAfterModalHide: true,
+ onSelected: () =>
+ Modal.close(() => {
+ if (Browser.isSafari()) {
+ return;
+ }
+ openPicker({
+ onPicked: showAvatarCropModal,
+ });
+ }),
},
];
@@ -344,14 +344,14 @@ function AvatarWithImagePicker({
menuItems.push({
icon: Expensicons.Eye,
text: translate('avatarWithImagePicker.viewPhoto'),
- onSelected: () => {
- if (typeof onViewPhotoPress !== 'function') {
- show();
- return;
- }
- onViewPhotoPress();
- },
- shouldCallAfterModalHide: true,
+ onSelected: () =>
+ Modal.close(() => {
+ if (typeof onViewPhotoPress !== 'function') {
+ show();
+ return;
+ }
+ onViewPhotoPress();
+ }),
});
}
diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx
index 74b38f515a06..943d6dbb5c16 100644
--- a/src/components/ButtonWithDropdownMenu/index.tsx
+++ b/src/components/ButtonWithDropdownMenu/index.tsx
@@ -11,6 +11,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import mergeRefs from '@libs/mergeRefs';
+import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type {ButtonWithDropdownMenuProps} from './types';
@@ -177,12 +178,11 @@ function ButtonWithDropdownMenu({
menuItems={options.map((item, index) => ({
...item,
onSelected: item.onSelected
- ? () => item.onSelected?.()
+ ? () => Modal.close(() => item.onSelected?.())
: () => {
onOptionSelected?.(item);
setSelectedItemIndex(index);
},
- shouldCallAfterModalHide: true,
}))}
/>
)}
diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
index 82e7d4f30a85..686c318a99dc 100644
--- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
+++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
@@ -33,6 +33,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [
SCREENS.WORKSPACE.TAXES,
SCREENS.WORKSPACE.REPORT_FIELDS,
SCREENS.WORKSPACE.EXPENSIFY_CARD,
+ SCREENS.WORKSPACE.COMPANY_CARDS,
SCREENS.WORKSPACE.DISTANCE_RATES,
SCREENS.SEARCH.CENTRAL_PANE,
SCREENS.SETTINGS.TROUBLESHOOT,
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 3b7b2068acd1..afce7f519ce5 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -51,7 +51,6 @@ import CheckmarkCircle from '@assets/images/simple-illustrations/simple-illustra
import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg';
import Coins from '@assets/images/simple-illustrations/simple-illustration__coins.svg';
import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg';
-import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__company-card.svg';
import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg';
import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg';
import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg';
@@ -102,6 +101,7 @@ import Tire from '@assets/images/simple-illustrations/simple-illustration__tire.
import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg';
import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg';
import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg';
+import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__twocards-horizontal.svg';
import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg';
import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg';
import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg';
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index a1ea786cc90d..0846e27100da 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -8,7 +8,6 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
@@ -36,11 +35,6 @@ type PopoverMenuItem = MenuItemProps & {
/** Determines whether the menu item is disabled or not */
disabled?: boolean;
-
- /** Determines whether the menu item's onSelected() function is called after the modal is hidden
- * It is meant to be used in situations where, after clicking on the modal, another one is opened.
- */
- shouldCallAfterModalHide?: boolean;
};
type PopoverModalProps = Pick;
@@ -134,11 +128,6 @@ function PopoverMenu({
setEnteredSubMenuIndexes([...enteredSubMenuIndexes, index]);
const selectedSubMenuItemIndex = selectedItem?.subMenuItems.findIndex((option) => option.isSelected);
setFocusedIndex(selectedSubMenuItemIndex);
- } else if (selectedItem.shouldCallAfterModalHide) {
- Modal.close(() => {
- onItemSelected(selectedItem, index);
- selectedItem.onSelected?.();
- });
} else {
onItemSelected(selectedItem, index);
selectedItem.onSelected?.();
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 8a8fc815ddb4..37b0c2f36c63 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2112,6 +2112,7 @@ export default {
expensifyCard: 'Expensify Card',
workflows: 'Workflows',
workspace: 'Workspace',
+ companyCards: 'Company cards',
edit: 'Edit workspace',
enabled: 'Enabled',
disabled: 'Disabled',
@@ -2876,6 +2877,13 @@ export default {
ctaTitle: 'Issue new card',
},
},
+ companyCards: {
+ title: 'Company Cards',
+ subtitle: 'Import spend from existing company cards',
+ disableCardTitle: 'Disable Company Cards',
+ disableCardPrompt: 'You can’t disable company cards because this feature is in use. Reach out to the Concierge for next steps.',
+ disableCardButton: 'Chat with Concierge',
+ },
workflows: {
title: 'Workflows',
subtitle: 'Configure how spend is approved and paid.',
@@ -3559,6 +3567,11 @@ export default {
description: `Add tax codes to your taxes for easy export of expenses to your accounting and payroll systems.`,
onlyAvailableOnPlan: 'Tax codes are only available on the Control plan, starting at ',
},
+ companyCards: {
+ title: 'Company Cards',
+ description: `Company cards lets you import spend for existing company cards from all major card issuers. You can assign cards to employees, and automatically import transactions.`,
+ onlyAvailableOnPlan: 'Company cards are only available on the Control plan, starting at ',
+ },
rules: {
title: 'Rules',
description: `Rules run in the background and keep your spend under control so you don't have to sweat the small stuff.\n\nRequire expense details like receipts and descriptions, set limits and defaults, and automate approvals and payments – all in one place.`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index a51ce1d91bf7..679a6297ffdd 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2143,6 +2143,7 @@ export default {
expensifyCard: 'Tarjeta Expensify',
workflows: 'Flujos de trabajo',
workspace: 'Espacio de trabajo',
+ companyCards: 'Tarjetas de empresa',
edit: 'Editar espacio de trabajo',
enabled: 'Activada',
disabled: 'Desactivada',
@@ -2920,6 +2921,13 @@ export default {
ctaTitle: 'Emitir nueva tarjeta',
},
},
+ companyCards: {
+ title: 'Tarjetas de empresa',
+ subtitle: 'Importar gastos de las tarjetas de empresa existentes.',
+ disableCardTitle: 'Deshabilitar tarjetas de empresa',
+ disableCardPrompt: 'No puedes deshabilitar las tarjetas de empresa porque esta función está en uso. Por favor, contacta a Concierge para los próximos pasos.',
+ disableCardButton: 'Chatear con Concierge',
+ },
distanceRates: {
title: 'Tasas de distancia',
subtitle: 'Añade, actualiza y haz cumplir las tasas.',
@@ -3608,6 +3616,11 @@ export default {
description: `Añada código de impuesto mayor a sus categorías para exportar fácilmente los gastos a sus sistemas de contabilidad y nómina.`,
onlyAvailableOnPlan: 'Los código de impuesto mayor solo están disponibles en el plan Control, a partir de ',
},
+ companyCards: {
+ title: 'Tarjetas de empresa',
+ description: `Las tarjetas de empresa le permiten importar los gastos de las tarjetas de empresa existentes de todos los principales emisores de tarjetas. Puede asignar tarjetas a empleados e importar transacciones automáticamente.`,
+ onlyAvailableOnPlan: 'Las tarjetas de empresa solo están disponibles en el plan Control, a partir de ',
+ },
rules: {
title: 'Reglas',
description: `Las reglas se ejecutan en segundo plano y mantienen tus gastos bajo control para que no tengas que preocuparte por los detalles pequeños.\n\nExige detalles de los gastos, como recibos y descripciones, establece límites y valores predeterminados, y automatiza las aprobaciones y los pagos, todo en un mismo lugar.`,
diff --git a/src/libs/API/parameters/EnablePolicyCompanyCardsParams.ts b/src/libs/API/parameters/EnablePolicyCompanyCardsParams.ts
new file mode 100644
index 000000000000..0bf3ce34b9d2
--- /dev/null
+++ b/src/libs/API/parameters/EnablePolicyCompanyCardsParams.ts
@@ -0,0 +1,7 @@
+type EnablePolicyCompanyCardsParams = {
+ authToken?: string | null;
+ policyID: string;
+ enabled: boolean;
+};
+
+export default EnablePolicyCompanyCardsParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index a72220c3d943..9696f4213a48 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -277,5 +277,6 @@ export type {default as ConfigureExpensifyCardsForPolicyParams} from './Configur
export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardParams';
export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams';
export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams';
+export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyCompanyCardsParams';
export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams';
export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index de63ed032afe..5ea2ae44b74d 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -199,6 +199,7 @@ const WRITE_COMMANDS = {
ENABLE_POLICY_WORKFLOWS: 'EnablePolicyWorkflows',
ENABLE_POLICY_REPORT_FIELDS: 'EnablePolicyReportFields',
ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards',
+ ENABLE_POLICY_COMPANY_CARDS: 'EnablePolicyCompanyCards',
ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing',
SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled',
SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax',
@@ -530,6 +531,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS]: Parameters.EnablePolicyWorkflowsParams;
[WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS]: Parameters.EnablePolicyReportFieldsParams;
[WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams;
+ [WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS]: Parameters.EnablePolicyCompanyCardsParams;
[WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams;
[WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams;
[WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index a9ac258a3533..4bbdac7b17b1 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -305,11 +305,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
const unsubscribeSearchShortcut = KeyboardShortcut.subscribe(
searchShortcutConfig.shortcutKey,
() => {
- Modal.close(
- Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER)),
- true,
- true,
- );
+ Modal.close(Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER)));
},
shortcutsOverviewShortcutConfig.descriptionKey,
shortcutsOverviewShortcutConfig.modifiers,
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 4694a2e73d5c..7722696245fd 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -514,18 +514,18 @@ const SearchReportModalStackNavigator = createModalStackNavigator({
[SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require('../../../../pages/Search/SearchFiltersDatePage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP]: () => require('../../../../pages/Search/SearchFiltersCurrencyPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: () => require('../../../../pages/Search/SearchFiltersDescriptionPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: () => require('../../../../pages/Search/SearchFiltersMerchantPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: () => require('../../../../pages/Search/SearchFiltersReportIDPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: () => require('../../../../pages/Search/SearchFiltersAmountPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: () => require('../../../../pages/Search/SearchFiltersCategoryPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: () => require('../../../../pages/Search/SearchFiltersKeywordPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: () => require('../../../../pages/Search/SearchFiltersCardPage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP]: () => require('../../../../pages/Search/SearchFiltersTaxRatePage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_EXPENSE_TYPE_RHP]: () => require('../../../../pages/Search/SearchFiltersExpenseTypePage').default,
- [SCREENS.SEARCH.ADVANCED_FILTERS_TAG_RHP]: () => require('../../../../pages/Search/SearchFiltersTagPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersDatePage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_CURRENCY_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersCurrencyPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_DESCRIPTION_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersDescriptionPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_MERCHANT_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_REPORT_ID_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersReportIDPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_AMOUNT_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_CATEGORY_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersCategoryPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_KEYWORD_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersKeywordPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_CARD_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_TAX_RATE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersTaxRatePage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_EXPENSE_TYPE_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersExpenseTypePage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_TAG_RHP]: () => require('../../../../pages/Search/SearchAdvancedFiltersPage/SearchFiltersTagPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_FROM_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersFromPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_TO_RHP]: () => require('@pages/Search/SearchAdvancedFiltersPage/SearchFiltersToPage').default,
});
diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
index 077f42d32ec5..22a190913ed2 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
@@ -32,6 +32,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = {
[SCREENS.WORKSPACE.TAXES]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default,
[SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsPage').default,
[SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default,
[SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default,
} satisfies Screens;
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 942a23068979..e4072ea1e696 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -160,6 +160,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
[SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE],
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: [],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [
SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW,
SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index bb9d92c7a5a3..4d3f19984b8f 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1060,6 +1060,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD.route,
},
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: {
+ path: ROUTES.WORKSPACE_COMPANY_CARDS.route,
+ },
[SCREENS.WORKSPACE.WORKFLOWS]: {
path: ROUTES.WORKSPACE_WORKFLOWS.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index c85f0972d84a..ceb62f1dac1c 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1119,6 +1119,9 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.WORKFLOWS]: {
policyID: string;
};
diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts
index 01ac832336ab..9cba7a359537 100644
--- a/src/libs/actions/Modal.ts
+++ b/src/libs/actions/Modal.ts
@@ -5,7 +5,6 @@ const closeModals: Array<(isNavigating?: boolean) => void> = [];
let onModalClose: null | (() => void);
let isNavigate: undefined | boolean;
-let shouldCloseAll: boolean | undefined;
/**
* Allows other parts of the app to call modal close function
@@ -40,13 +39,12 @@ function closeTop() {
/**
* Close modal in other parts of the app
*/
-function close(onModalCloseCallback: () => void, isNavigating = true, shouldCloseAllModals = false) {
+function close(onModalCloseCallback: () => void, isNavigating = true) {
if (closeModals.length === 0) {
onModalCloseCallback();
return;
}
onModalClose = onModalCloseCallback;
- shouldCloseAll = shouldCloseAllModals;
isNavigate = isNavigating;
closeTop();
}
@@ -55,7 +53,7 @@ function onModalDidClose() {
if (!onModalClose) {
return;
}
- if (closeModals.length && shouldCloseAll) {
+ if (closeModals.length) {
closeTop();
return;
}
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 19585a5e69c5..753428e4a6df 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -12,6 +12,7 @@ import type {
CreateWorkspaceParams,
DeleteWorkspaceAvatarParams,
DeleteWorkspaceParams,
+ EnablePolicyCompanyCardsParams,
EnablePolicyConnectionsParams,
EnablePolicyExpensifyCardsParams,
EnablePolicyInvoicingParams,
@@ -234,27 +235,24 @@ function getInvoicePrimaryWorkspace(activePolicyID?: OnyxEntry): Policy
/**
* Check if the user has any active free policies (aka workspaces)
*/
-function hasActiveChatEnabledPolicies(policies: Array> | OnyxCollection, includeOnlyFreePolicies = false): boolean {
- const adminChatEnabledPolicies = Object.values(policies ?? {}).filter(
- (policy) =>
- policy &&
- ((policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) ||
- (!includeOnlyFreePolicies && policy.type !== CONST.POLICY.TYPE.PERSONAL && policy.role === CONST.POLICY.ROLE.ADMIN && policy.isPolicyExpenseChatEnabled)),
+function hasActiveChatEnabledPolicies(policies: Array> | OnyxCollection, includeOnlyAdminPolicies = false): boolean {
+ const chatEnabledPolicies = Object.values(policies ?? {}).filter(
+ (policy) => policy?.isPolicyExpenseChatEnabled && (!includeOnlyAdminPolicies || policy.role === CONST.POLICY.ROLE.ADMIN),
);
- if (adminChatEnabledPolicies.length === 0) {
+ if (chatEnabledPolicies.length === 0) {
return false;
}
- if (adminChatEnabledPolicies.some((policy) => !policy?.pendingAction)) {
+ if (chatEnabledPolicies.some((policy) => !policy?.pendingAction)) {
return true;
}
- if (adminChatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) {
+ if (chatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) {
return true;
}
- if (adminChatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
+ if (chatEnabledPolicies.some((policy) => policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
return false;
}
@@ -2722,6 +2720,56 @@ function enableExpensifyCard(policyID: string, enabled: boolean) {
}
}
+function enableCompanyCards(policyID: string, enabled: boolean) {
+ const authToken = NetworkStore.getAuthToken();
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ areCompanyCardsEnabled: enabled,
+ pendingFields: {
+ areCompanyCardsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {
+ areCompanyCardsEnabled: null,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ areCompanyCardsEnabled: !enabled,
+ pendingFields: {
+ areCompanyCardsEnabled: null,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters: EnablePolicyCompanyCardsParams = {authToken, policyID, enabled};
+
+ API.write(WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, parameters, onyxData);
+
+ if (enabled && getIsNarrowLayout()) {
+ navigateWhenEnableFeature(policyID);
+ }
+}
+
function enablePolicyReportFields(policyID: string, enabled: boolean, disableRedirect = false) {
const onyxData: OnyxData = {
optimisticData: [
@@ -3429,6 +3477,7 @@ export {
setWorkspacePayer,
setWorkspaceReimbursement,
openPolicyWorkflowsPage,
+ enableCompanyCards,
enablePolicyConnections,
enablePolicyReportFields,
enablePolicyTaxes,
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 5143a2d70008..4b859e37785e 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -225,31 +225,33 @@ function AdvancedSearchFilters() {
};
return (
-
-
- {advancedFilters.map((item) => {
- const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));
- if (item.shouldHide) {
- return undefined;
- }
- return (
-
- );
- })}
-
+ <>
+
+
+ {advancedFilters.map((item) => {
+ const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));
+ if (item.shouldHide) {
+ return undefined;
+ }
+ return (
+
+ );
+ })}
+
+
-
+ >
);
}
diff --git a/src/pages/Search/SearchFiltersAmountPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx
similarity index 99%
rename from src/pages/Search/SearchFiltersAmountPage.tsx
rename to src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx
index d7533f9ac20e..56eb3dce4350 100644
--- a/src/pages/Search/SearchFiltersAmountPage.tsx
+++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx
@@ -44,6 +44,7 @@ function SearchFiltersAmountPage() {
shouldShowOfflineIndicatorInWideScreen
offlineIndicatorStyle={styles.mtAuto}
includeSafeAreaPaddingBottom={false}
+ shouldEnableMaxHeight
>
diff --git a/src/pages/Search/SearchFiltersKeywordPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersKeywordPage.tsx
similarity index 98%
rename from src/pages/Search/SearchFiltersKeywordPage.tsx
rename to src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersKeywordPage.tsx
index 3a0fb0a67367..41b1bcbc943b 100644
--- a/src/pages/Search/SearchFiltersKeywordPage.tsx
+++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersKeywordPage.tsx
@@ -36,6 +36,7 @@ function SearchFiltersKeywordPage() {
testID={SearchFiltersKeywordPage.displayName}
shouldShowOfflineIndicatorInWideScreen
offlineIndicatorStyle={styles.mtAuto}
+ shouldEnableMaxHeight
>
diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
index 6e3c3a48de74..5d7b5b1390c2 100644
--- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
+++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
@@ -23,6 +23,7 @@ import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import * as SubscriptionUtils from '@libs/SubscriptionUtils';
import * as IOU from '@userActions/IOU';
+import * as Modal from '@userActions/Modal';
import * as Report from '@userActions/Report';
import * as Task from '@userActions/Task';
import type {IOUType} from '@src/CONST';
@@ -224,13 +225,13 @@ function AttachmentPickerWithMenuItems({
{
icon: Expensicons.Paperclip,
text: translate('reportActionCompose.addAttachment'),
- onSelected: () => {
- if (Browser.isSafari()) {
- return;
- }
- triggerAttachmentPicker();
- },
- shouldCallAfterModalHide: true,
+ onSelected: () =>
+ Modal.close(() => {
+ if (Browser.isSafari()) {
+ return;
+ }
+ triggerAttachmentPicker();
+ }),
},
];
return (
diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
index 802967345fb6..71c42acefdaa 100644
--- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
+++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
@@ -23,6 +23,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';
+import * as Modal from '@userActions/Modal';
import * as Transaction from '@userActions/Transaction';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
@@ -178,10 +179,11 @@ function IOURequestStepWaypoint({
icon: Expensicons.Trashcan,
text: translate('distance.deleteWaypoint'),
onSelected: () => {
- setRestoreFocusType(undefined);
- setIsDeleteStopModalOpen(true);
+ Modal.close(() => {
+ setRestoreFocusType(undefined);
+ setIsDeleteStopModalOpen(true);
+ });
},
- shouldCallAfterModalHide: true,
},
]}
/>
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 76ef67bdb0f0..c3a384fa5d8b 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -65,6 +65,7 @@ type WorkspaceMenuItem = {
| typeof SCREENS.WORKSPACE.PROFILE
| typeof SCREENS.WORKSPACE.MEMBERS
| typeof SCREENS.WORKSPACE.EXPENSIFY_CARD
+ | typeof SCREENS.WORKSPACE.COMPANY_CARDS
| typeof SCREENS.WORKSPACE.REPORT_FIELDS
| typeof SCREENS.WORKSPACE.RULES;
};
@@ -112,6 +113,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
[CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED]: policy?.areCategoriesEnabled,
[CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]: policy?.areTagsEnabled,
[CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]: policy?.tax?.trackingEnabled,
+ [CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]: policy?.areCompanyCardsEnabled,
[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections),
[CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]: policy?.areExpensifyCardsEnabled,
[CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled,
@@ -256,6 +258,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
});
}
+ if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]) {
+ protectedCollectPolicyMenuItems.push({
+ translationKey: 'workspace.common.companyCards',
+ icon: Expensicons.CreditCard,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)))),
+ routeName: SCREENS.WORKSPACE.COMPANY_CARDS,
+ });
+ }
+
if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED]) {
protectedCollectPolicyMenuItems.push({
translationKey: 'workspace.common.workflows',
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index d33a83c4363c..5b356f768dd3 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -71,10 +71,12 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
const policyID = policy?.id;
const workspaceAccountID = policy?.workspaceAccountID ?? -1;
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID.toString()}${CONST.EXPENSIFY_CARD.BANK}`);
+ const [companyCardsList] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID.toString()}`);
const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false);
const [isIntegrateWarningModalOpen, setIsIntegrateWarningModalOpen] = useState(false);
const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false);
const [isDisableExpensifyCardWarningModalOpen, setIsDisableExpensifyCardWarningModalOpen] = useState(false);
+ const [isDisableCompanyCardsWarningModalOpen, setIsDisableCompanyCardsWarningModalOpen] = useState(false);
const spendItems: Item[] = [
{
@@ -94,7 +96,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
// TODO remove this when feature will be fully done, and move spend item inside spendItems array
if (canUseWorkspaceFeeds) {
- spendItems.splice(1, 0, {
+ spendItems.push({
icon: Illustrations.HandCard,
titleTranslationKey: 'workspace.moreFeatures.expensifyCard.title',
subtitleTranslationKey: 'workspace.moreFeatures.expensifyCard.subtitle',
@@ -111,6 +113,29 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
setIsDisableExpensifyCardWarningModalOpen(true);
},
});
+ spendItems.push({
+ icon: Illustrations.CompanyCard,
+ titleTranslationKey: 'workspace.moreFeatures.companyCards.title',
+ subtitleTranslationKey: 'workspace.moreFeatures.companyCards.subtitle',
+ isActive: policy?.areCompanyCardsEnabled ?? false,
+ pendingAction: policy?.pendingFields?.areCompanyCardsEnabled,
+ disabled: !isEmptyObject(companyCardsList),
+ action: (isEnabled: boolean) => {
+ if (!policyID) {
+ return;
+ }
+ if (isEnabled && !isControlPolicy(policy)) {
+ Navigation.navigate(
+ ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)),
+ );
+ return;
+ }
+ Policy.enableCompanyCards(policyID, isEnabled);
+ },
+ disabledAction: () => {
+ setIsDisableCompanyCardsWarningModalOpen(true);
+ },
+ });
}
const manageItems: Item[] = [
@@ -444,6 +469,18 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
confirmText={translate('workspace.moreFeatures.expensifyCard.disableCardButton')}
cancelText={translate('common.cancel')}
/>
+ {
+ setIsDisableCompanyCardsWarningModalOpen(false);
+ Report.navigateToConciergeChat(true);
+ }}
+ onCancel={() => setIsDisableCompanyCardsWarningModalOpen(false)}
+ prompt={translate('workspace.moreFeatures.companyCards.disableCardPrompt')}
+ confirmText={translate('workspace.moreFeatures.companyCards.disableCardButton')}
+ cancelText={translate('common.cancel')}
+ />
);
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index d4ff46e268f8..94fb454c4a2d 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -32,6 +32,7 @@ import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import type {AvatarSource} from '@libs/UserUtils';
import * as App from '@userActions/App';
+import * as Modal from '@userActions/Modal';
import * as Policy from '@userActions/Policy/Policy';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
@@ -161,12 +162,12 @@ function WorkspacesListPage({policies, reimbursementAccount, reports, session}:
threeDotsMenuItems.push({
icon: Expensicons.Trashcan,
text: translate('workspace.common.delete'),
- onSelected: () => {
- setPolicyIDToDelete(item.policyID ?? '-1');
- setPolicyNameToDelete(item.title);
- setIsDeleteModalOpen(true);
- },
- shouldCallAfterModalHide: true,
+ onSelected: () =>
+ Modal.close(() => {
+ setPolicyIDToDelete(item.policyID ?? '-1');
+ setPolicyNameToDelete(item.title);
+ setIsDeleteModalOpen(true);
+ }),
});
}
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index 107f64dd2dee..11c47a21f2e1 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -38,6 +38,7 @@ import Navigation from '@navigation/Navigation';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import type {AnchorPosition} from '@styles/index';
+import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -97,8 +98,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
{
icon: Expensicons.Key,
text: translate('workspace.accounting.enterCredentials'),
- onSelected: () => startIntegrationFlow({name: connectedIntegration}),
- shouldCallAfterModalHide: true,
+ onSelected: () => Modal.close(() => startIntegrationFlow({name: connectedIntegration})),
disabled: isOffline,
iconRight: Expensicons.NewWindow,
shouldShowRightIcon: true,
@@ -115,8 +115,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
{
icon: Expensicons.Trashcan,
text: translate('workspace.accounting.disconnect'),
- onSelected: () => setIsDisconnectModalOpen(true),
- shouldCallAfterModalHide: true,
+ onSelected: () => Modal.close(() => setIsDisconnectModalOpen(true)),
},
],
[shouldShowEnterCredentials, translate, isOffline, policyID, connectedIntegration, startIntegrationFlow],
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
new file mode 100644
index 000000000000..1945cf99a001
--- /dev/null
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
@@ -0,0 +1,42 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import {View} from 'react-native';
+import * as Illustrations from '@components/Icon/Illustrations';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
+import CONST from '@src/CONST';
+import type SCREENS from '@src/SCREENS';
+
+type WorkspaceCompanyCardPageProps = StackScreenProps;
+
+function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ return (
+
+
+
+
+
+ );
+}
+
+WorkspaceCompanyCardPage.displayName = 'WorkspaceCompanyCardPage';
+
+export default WorkspaceCompanyCardPage;
diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx
index 91f069ac2224..b6da4dd689e6 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -21,6 +21,7 @@ type PolicyRoute = RouteProp<
| typeof SCREENS.WORKSPACE.MORE_FEATURES
| typeof SCREENS.WORKSPACE.MEMBERS
| typeof SCREENS.WORKSPACE.EXPENSIFY_CARD
+ | typeof SCREENS.WORKSPACE.COMPANY_CARDS
| typeof SCREENS.WORKSPACE.INVITE
| typeof SCREENS.WORKSPACE.INVITE_MESSAGE
| typeof SCREENS.WORKSPACE.WORKFLOWS_PAYER
diff --git a/src/types/onyx/CompanyCards.ts b/src/types/onyx/CompanyCards.ts
new file mode 100644
index 000000000000..17ebbaf98bb1
--- /dev/null
+++ b/src/types/onyx/CompanyCards.ts
@@ -0,0 +1,41 @@
+/** Model of CompanyCard's Shared NVP record */
+// TODO update information here during implementation Add Company Card flow
+type CompanyCards = {
+ /** Company cards object */
+ companyCards: {
+ /** Company card info key */
+ cdfbmo: CompanyCardInfo;
+ };
+ /** Company cards nicknames */
+ companyCardNicknames: {
+ /** Company cards info key */
+ cdfbmo: string;
+ };
+};
+/**
+ * Model of company card information
+ */
+type CompanyCardInfo = {
+ /** Company card pending state */
+ pending: boolean;
+
+ /** Company card asr state */
+ asrEnabled: boolean;
+
+ /** Company card force reimbursable value */
+ forceReimbursable: string;
+
+ /** Company card liability type */
+ liabilityType: string;
+
+ /** Company card preferred policy */
+ preferredPolicy: string;
+
+ /** Company card report title format */
+ reportTitleFormat: string;
+
+ /** Company card statement period */
+ statementPeriodEndDay: string;
+};
+
+export default CompanyCards;
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 9bac5f2e4de4..532be964ad23 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -1522,6 +1522,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Whether the Invoices feature is enabled */
areInvoicesEnabled?: boolean;
+ /** Whether the Company Cards feature is enabled */
+ areCompanyCardsEnabled?: boolean;
+
/** The verified bank account linked to the policy */
achAccount?: ACHAccount;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 2bb129708981..ca7dc271f84a 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -10,6 +10,7 @@ import type BlockedFromConcierge from './BlockedFromConcierge';
import type CancellationDetails from './CancellationDetails';
import type Card from './Card';
import type {CardList, IssueNewCard, WorkspaceCardsList} from './Card';
+import type CompanyCards from './CompanyCards';
import type {CapturedLogs, Log} from './Console';
import type Credentials from './Credentials';
import type Currency from './Currency';
@@ -114,6 +115,7 @@ export type {
Credentials,
Currency,
CurrencyList,
+ CompanyCards,
CustomStatusDraft,
DismissedReferralBanners,
Download,