Skip to content

Commit c901141

Browse files
authored
Merge branch 'Expensify:main' into krishna/89835-confirmation-rate-selection-ux
2 parents 08eccad + 7020187 commit c901141

87 files changed

Lines changed: 1458 additions & 784 deletions

File tree

Some content is hidden

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

config/eslint/eslint.seatbelt.tsv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,9 @@
377377
"../../src/components/Search/SearchList/ListItem/ChatListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
378378
"../../src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5
379379
"../../src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
380-
"../../src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
381380
"../../src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
382381
"../../src/components/Search/SearchList/ListItem/GroupHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
382+
"../../src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
383383
"../../src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
384384
"../../src/components/Search/SearchList/ListItem/TaskListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
385385
"../../src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
@@ -448,7 +448,7 @@
448448
"../../src/components/Table/TableContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
449449
"../../src/components/Table/TableFilterButtons/buildFilterItems.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
450450
"../../src/components/Table/middlewares/filtering.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
451-
"../../src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
451+
"../../src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
452452
"../../src/components/TagPicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
453453
"../../src/components/TaxPicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
454454
"../../src/components/TestDrive/Modal/AdminTestDriveModal.tsx" "no-restricted-imports" 1

patches/react-native/details.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,11 @@
284284
- Upstream PR/issue: https://github.com/facebook/react-native/issues/53128
285285
- E/App issue: https://github.com/Expensify/App/issues/91292
286286
- PR introducing patch: https://github.com/Expensify/App/pull/91736
287+
288+
289+
### [react-native+0.83.1+038+fix-nil-BlobModule-crash-APP-8BM.patch](react-native+0.83.1+038+fix-nil-BlobModule-crash-APP-8BM.patch)
290+
291+
- Reason: Fixes a fatal iOS crash (APP-8BM) in HybridApp where `RCTNetworking`'s default URL-request-handler provider builds its handler list using an Objective-C array literal (`@[...]`) with `[moduleRegistry moduleForName:"BlobModule"]` at index 3. During OldDot↔NewDot bridge transitions, the `__weak _turboModuleRegistry` in `RCTModuleRegistry` is zeroed by ARC at the start of `TurboModuleManager` dealloc — before `[RCTNetworking invalidate]` clears the handler cache — leaving a window where a concurrent in-flight network request calls `prioritizedHandlers`, finds the cache empty, and tries to rebuild it with a nil `BlobModule`. Since `@[…]` compiles to `+[NSArray arrayWithObjects:count:]` which raises `NSInvalidArgumentException` on any nil element, the crash is fatal. The fix replaces the literal with an `NSMutableArray` built from the three always-non-nil handlers (`RCTHTTPRequestHandler`, `RCTDataRequestHandler`, `RCTFileRequestHandler`) and conditionally appends `BlobModule` only when the registry lookup is non-nil, turning a guaranteed crash into a graceful "no blob handler for this window".
292+
- Upstream PR/issue: 🛑
293+
- E/App issue: https://github.com/Expensify/App/issues/92413
294+
- PR introducing patch: https://github.com/Expensify/App/pull/92918
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
diff --git a/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm b/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
2+
index fabfff3..cefa0fa 100644
3+
--- a/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
4+
+++ b/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
5+
@@ -103,11 +103,16 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
6+
initWithHandlersProvider:^NSArray<id<RCTURLRequestHandler>> *(RCTModuleRegistry *moduleRegistry) {
7+
NSArray *URLRequestHandlerModules =
8+
extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTURLRequestHandler));
9+
- return [@[
10+
+ NSMutableArray<id<RCTURLRequestHandler>> *defaultHandlers = [NSMutableArray arrayWithObjects:
11+
[RCTHTTPRequestHandler new],
12+
[RCTDataRequestHandler new],
13+
[RCTFileRequestHandler new],
14+
- [moduleRegistry moduleForName:"BlobModule"],
15+
- ] arrayByAddingObjectsFromArray:URLRequestHandlerModules];
16+
+ nil
17+
+ ];
18+
+ id blobModule = [moduleRegistry moduleForName:"BlobModule"];
19+
+ if (blobModule != nil) {
20+
+ [defaultHandlers addObject:blobModule];
21+
+ }
22+
+ return [defaultHandlers arrayByAddingObjectsFromArray:URLRequestHandlerModules];
23+
}];
24+
}

src/CONST/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3982,6 +3982,7 @@ const CONST = {
39823982
MAKE_MEMBER: 'makeMember',
39833983
MAKE_ADMIN: 'makeAdmin',
39843984
MAKE_AUDITOR: 'makeAuditor',
3985+
MAKE_CARD_ADMIN: 'makeCardAdmin',
39853986
},
39863987
BULK_ACTION_TYPES: {
39873988
DELETE: 'delete',
@@ -7440,12 +7441,12 @@ const CONST = {
74407441
description: 'workspace.upgrade.distanceRates.description' as const,
74417442
icon: 'CarIce',
74427443
},
7443-
auditor: {
7444-
id: 'auditor' as const,
7445-
alias: 'auditor',
7446-
name: 'Auditor',
7447-
title: 'workspace.upgrade.auditor.title' as const,
7448-
description: 'workspace.upgrade.auditor.description' as const,
7444+
controlPolicyRoles: {
7445+
id: 'controlPolicyRoles' as const,
7446+
alias: 'control-policy-roles',
7447+
name: 'Control policy roles',
7448+
title: 'workspace.upgrade.controlPolicyRoles.title' as const,
7449+
description: 'workspace.upgrade.controlPolicyRoles.description' as const,
74497450
icon: 'BlueShield',
74507451
},
74517452
reports: {

src/components/Search/SearchAutocompleteList.tsx

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ type SearchAutocompleteListProps = {
6868
/** Whether to subscribe to KeyboardShortcut arrow keys events */
6969
shouldSubscribeToArrowKeyEvents?: boolean;
7070

71-
/** Callback to highlight (e.g. scroll to) the first matched item in the list. */
72-
onHighlightFirstItem?: () => void;
71+
/** Whether to highlight the first matched result so Enter selects it. Only the SearchRouter (Cmd+K) uses this;
72+
* the search page input keeps focus on the search-query row to match production behavior. */
73+
shouldHighlightFirstItem?: boolean;
7374

7475
/** Ref for the external text input */
7576
textInputRef?: RefObject<AnimatedTextInputRef | null>;
@@ -145,7 +146,7 @@ function SearchAutocompleteList({
145146
getAdditionalSections,
146147
onListItemPress,
147148
shouldSubscribeToArrowKeyEvents = true,
148-
onHighlightFirstItem,
149+
shouldHighlightFirstItem = false,
149150
textInputRef,
150151
autocompleteSubstitutions,
151152
ref,
@@ -273,8 +274,8 @@ function SearchAutocompleteList({
273274
// effect can re-fire and correctly focus the first focusable item (skipping section headers).
274275
hasSetInitialFocusRef.current = false;
275276
} else {
276-
// When query changes to a non-empty value, focus on the search query item (index 0) and scroll to top
277-
// onHighlightFirstItem will switch focus to the first result when there's a good match
277+
// When query changes to a non-empty value, focus on the search query item (index 0) and scroll to top.
278+
// The highlight effect below switches focus to the first result when there's a good match.
278279
innerListRef.current?.updateAndScrollToFocusedIndex(0, true);
279280
}
280281
}
@@ -570,8 +571,6 @@ function SearchAutocompleteList({
570571
reports,
571572
]);
572573

573-
const sectionItemText = sections?.at(1)?.data?.[0]?.text ?? '';
574-
const normalizedReferenceText = sectionItemText.toLowerCase();
575574
const trimmedAutocompleteQueryValue = autocompleteQueryValue.trim();
576575
const isLoading = !isRecentSearchesDataLoaded;
577576
const suggestionsAnnouncement = suggestionsCount > 0 ? translate('search.suggestionsAvailable', {count: suggestionsCount}, trimmedAutocompleteQueryValue) : '';
@@ -581,29 +580,37 @@ function SearchAutocompleteList({
581580
const shouldAnnounceNoResults = !isLoading && suggestionsCount === 0 && !!trimmedAutocompleteQueryValue;
582581
useDebouncedAccessibilityAnnouncement(noResultsFoundText, shouldAnnounceNoResults, autocompleteQueryValue);
583582

584-
const firstRecentReportKey = styledRecentReports.at(0)?.keyForList;
583+
// Locate the first recent report row in the order it is actually rendered. The two-section switcher sorts the
584+
// local "Recent chats" rows by a frozen rank, so the rendered order can differ from styledRecentReports (the
585+
// unsorted combined local + server list). Walking sections keeps the focused row, its reference text, and the
586+
// initially focused key all pointing at the first row the user actually sees.
587+
const recentReportKeys = new Set(styledRecentReports.map((report) => report.keyForList));
588+
let firstRecentReportKey: string | undefined;
589+
let firstRecentReportText = '';
585590
let firstRecentReportFlatIndex = -1;
586-
if (firstRecentReportKey) {
587-
let flatIndex = 0;
588-
for (const section of sections) {
589-
const hasData = (section.data?.length ?? 0) > 0;
590-
const hasHeader = hasData && (section.title !== undefined || ('customHeader' in section && section.customHeader !== undefined));
591-
if (hasHeader) {
592-
flatIndex++;
593-
}
594-
for (const item of section.data ?? []) {
595-
if (item.keyForList === firstRecentReportKey) {
596-
firstRecentReportFlatIndex = flatIndex;
597-
break;
598-
}
599-
flatIndex++;
600-
}
601-
if (firstRecentReportFlatIndex !== -1) {
591+
let flatIndex = 0;
592+
for (const section of sections) {
593+
const hasData = (section.data?.length ?? 0) > 0;
594+
const hasHeader = hasData && (section.title !== undefined || ('customHeader' in section && section.customHeader !== undefined));
595+
if (hasHeader) {
596+
flatIndex++;
597+
}
598+
for (const item of section.data ?? []) {
599+
if (item.keyForList && recentReportKeys.has(item.keyForList)) {
600+
firstRecentReportKey = item.keyForList;
601+
firstRecentReportText = item.text ?? '';
602+
firstRecentReportFlatIndex = flatIndex;
602603
break;
603604
}
605+
flatIndex++;
606+
}
607+
if (firstRecentReportFlatIndex !== -1) {
608+
break;
604609
}
605610
}
606611

612+
const normalizedReferenceText = firstRecentReportText.toLowerCase();
613+
607614
// When options initialize after the list is already mounted, initiallyFocusedItemKey has no effect
608615
// because useState(initialFocusedIndex) in useArrowKeyFocusManager only reads the initial value.
609616
// Imperatively focus the first recent report once options become available (desktop only).
@@ -619,10 +626,13 @@ function SearchAutocompleteList({
619626
useEffect(() => {
620627
const targetText = autocompleteQueryValue;
621628

622-
if (shouldHighlight(normalizedReferenceText, targetText)) {
623-
onHighlightFirstItem?.();
629+
if (!shouldHighlightFirstItem || firstRecentReportFlatIndex === -1 || !shouldHighlight(normalizedReferenceText, targetText)) {
630+
return;
624631
}
625-
}, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]);
632+
// Focus the header-aware flat index of the first result. A fixed index (e.g. searchQueryItems.length)
633+
// lands on the "Recent chats" section header row after the two-section switcher was introduced.
634+
innerListRef.current?.updateAndScrollToFocusedIndex(firstRecentReportFlatIndex, true);
635+
}, [autocompleteQueryValue, firstRecentReportFlatIndex, normalizedReferenceText, shouldHighlightFirstItem]);
626636

627637
if (isLoading) {
628638
return (

src/components/Search/SearchRouter/SearchRouter.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,6 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
375375
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => {
376376
onRouterClose();
377377
});
378-
const updateAndScrollToFocusedIndex = useCallback(() => listRef.current?.updateAndScrollToFocusedIndex(searchQueryItems?.length ?? 1, true), [searchQueryItems?.length]);
379378

380379
const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth};
381380

@@ -424,7 +423,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
424423
searchQueryItems={searchQueryItems}
425424
getAdditionalSections={getAdditionalSections}
426425
onListItemPress={onListItemPress}
427-
onHighlightFirstItem={updateAndScrollToFocusedIndex}
426+
shouldHighlightFirstItem
428427
ref={listRef}
429428
textInputRef={textInputRef}
430429
autocompleteSubstitutions={autocompleteSubstitutions}

src/components/SpendRules/configuration/SpendRuleMaxAmountBase.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
88
import ScreenWrapper from '@components/ScreenWrapper';
99
import Text from '@components/Text';
1010
import useAutoFocusInput from '@hooks/useAutoFocusInput';
11+
import useCanWriteCardSpendRules from '@hooks/useCanWriteCardSpendRules';
1112
import useLocalize from '@hooks/useLocalize';
1213
import useThemeStyles from '@hooks/useThemeStyles';
1314
import Navigation from '@libs/Navigation/Navigation';
@@ -27,6 +28,7 @@ function SpendRuleMaxAmountBase({policyID, maxAmount, currencyCode, onMaxAmountC
2728
const styles = useThemeStyles();
2829
const {translate} = useLocalize();
2930
const {inputCallbackRef} = useAutoFocusInput();
31+
const canWriteCardSpendRules = useCanWriteCardSpendRules(policyID);
3032

3133
const goBack = () => {
3234
Navigation.goBack();
@@ -40,8 +42,9 @@ function SpendRuleMaxAmountBase({policyID, maxAmount, currencyCode, onMaxAmountC
4042
return (
4143
<AccessOrNotFoundWrapper
4244
policyID={policyID}
43-
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
45+
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.PAID]}
4446
featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED}
47+
shouldBeBlocked={!canWriteCardSpendRules}
4548
>
4649
<ScreenWrapper
4750
shouldEnableMaxHeight

src/components/SpendRules/configuration/SpendRuleMerchantEditBase.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {ListItem} from '@components/SelectionList/ListItem/types';
1212
import Text from '@components/Text';
1313
import TextInput from '@components/TextInput';
1414
import useAutoFocusInput from '@hooks/useAutoFocusInput';
15+
import useCanWriteCardSpendRules from '@hooks/useCanWriteCardSpendRules';
1516
import useLocalize from '@hooks/useLocalize';
1617
import useThemeStyles from '@hooks/useThemeStyles';
1718
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
@@ -37,6 +38,7 @@ function SpendRuleMerchantEditBase({policyID, merchantIndex, merchantMatchTypes,
3738
const {translate} = useLocalize();
3839
const styles = useThemeStyles();
3940
const {inputCallbackRef} = useAutoFocusInput();
41+
const canWriteCardSpendRules = useCanWriteCardSpendRules(policyID);
4042

4143
const isNew = merchantIndex === ROUTES.NEW;
4244
const index = isNew ? -1 : Number(merchantIndex);
@@ -98,7 +100,8 @@ function SpendRuleMerchantEditBase({policyID, merchantIndex, merchantMatchTypes,
98100
<AccessOrNotFoundWrapper
99101
policyID={policyID}
100102
featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED}
101-
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
103+
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.PAID]}
104+
shouldBeBlocked={!canWriteCardSpendRules}
102105
>
103106
<ScreenWrapper
104107
testID="SpendRuleMerchantEditPage"

src/components/SpendRules/configuration/SpendRuleMerchantsBase.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import MenuItem from '@components/MenuItem';
77
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
88
import ScreenWrapper from '@components/ScreenWrapper';
99
import ScrollView from '@components/ScrollView';
10+
import useCanWriteCardSpendRules from '@hooks/useCanWriteCardSpendRules';
1011
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
1112
import useLocalize from '@hooks/useLocalize';
1213
import useThemeStyles from '@hooks/useThemeStyles';
@@ -29,6 +30,7 @@ function SpendRuleMerchantsBase({policyID, action, merchantMatchTypes, merchantN
2930
const {translate} = useLocalize();
3031
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Plus']);
3132
const illustrations = useMemoizedLazyIllustrations(['FoodTruck']);
33+
const canWriteCardSpendRules = useCanWriteCardSpendRules(policyID);
3234

3335
const emptyStateTitle =
3436
action === CONST.SPEND_RULES.ACTION.BLOCK ? translate('workspace.rules.spendRules.noBlockedMerchants') : translate('workspace.rules.spendRules.noAllowedMerchants');
@@ -52,7 +54,8 @@ function SpendRuleMerchantsBase({policyID, action, merchantMatchTypes, merchantN
5254
<AccessOrNotFoundWrapper
5355
policyID={policyID}
5456
featureName={CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED}
55-
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
57+
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.PAID]}
58+
shouldBeBlocked={!canWriteCardSpendRules}
5659
>
5760
<ScreenWrapper
5861
testID="SpendRuleMerchantsPage"

src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import useLocalize from '@hooks/useLocalize';
1313
import useNetwork from '@hooks/useNetwork';
1414
import useTheme from '@hooks/useTheme';
1515
import useThemeStyles from '@hooks/useThemeStyles';
16-
import {formatMaskedCardName, getCardFeedWithDomainID} from '@libs/CardUtils';
16+
import {formatMaskedCardName} from '@libs/CardUtils';
1717
import Navigation from '@libs/Navigation/Navigation';
1818
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
1919
import variables from '@styles/variables';
@@ -45,6 +45,9 @@ type WorkspaceCompanyCardTableRowProps = {
4545
/** Policy ID */
4646
policyID: string;
4747

48+
/** Selected card feed */
49+
feedName?: CompanyCardFeedWithDomainID;
50+
4851
/** Card feed icon element */
4952
CardFeedIcon?: React.ReactNode;
5053

@@ -71,6 +74,7 @@ type WorkspaceCompanyCardTableRowProps = {
7174
function WorkspaceCompanyCardTableRow({
7275
item,
7376
policyID,
77+
feedName,
7478
CardFeedIcon,
7579
shouldUseNarrowTableLayout,
7680
rowIndex,
@@ -108,7 +112,7 @@ function WorkspaceCompanyCardTableRow({
108112
? {width: variables.cardAvatarWidth, height: variables.cardAvatarHeight}
109113
: {width: variables.cardAvatarWidthSmall, height: variables.cardAvatarHeightSmall};
110114

111-
const canOpenCardDetails = !!assignedCard?.accountID && !!assignedCard?.fundID && assignedCard?.cardID !== undefined;
115+
const canOpenCardDetails = !!assignedCard?.accountID && assignedCard?.cardID !== undefined && !!feedName;
112116
const canAssignCard = !isAssigned && canWriteCompanyCards && !isAssigningCardDisabled;
113117
const canPressRow = canOpenCardDetails || canAssignCard;
114118

@@ -122,14 +126,12 @@ function WorkspaceCompanyCardTableRow({
122126
return;
123127
}
124128

125-
const {cardID, fundID} = assignedCard;
126-
if (!canOpenCardDetails || cardID === undefined || !fundID) {
129+
const {cardID} = assignedCard;
130+
if (!canOpenCardDetails || cardID === undefined || !feedName) {
127131
return;
128132
}
129133

130-
const feedName = getCardFeedWithDomainID(assignedCard?.bank as CompanyCardFeed, fundID);
131-
132-
return Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, feedName as CompanyCardFeedWithDomainID, cardID.toString()));
134+
return Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, feedName, cardID.toString()));
133135
};
134136

135137
return (

0 commit comments

Comments
 (0)