Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7f2cc8a
Create domain validation pages
mhawryluk Oct 20, 2025
92832de
Merge branch 'feat/saml-display-domain-list' into feat/domain-validation
mhawryluk Oct 22, 2025
6f14e73
Merge branch 'main' into feat/domain-validation
mhawryluk Oct 23, 2025
4ce735b
Extend navigation, actions and loading states
mhawryluk Oct 27, 2025
47c3a3c
Add link and error message to Verify Domain RHP
mhawryluk Oct 27, 2025
7188e71
Display error from BE in Verify Domain page
mhawryluk Oct 27, 2025
3ece949
Fix RHP refresh behavior
mhawryluk Oct 27, 2025
3c1cc9c
Display verification badge only to admins
mhawryluk Oct 27, 2025
b9ed09a
Support offline status in Verify Domain RHP
mhawryluk Oct 27, 2025
3f8cf0e
Normalize workspaces path in getMatchingFullScreenRoute
mhawryluk Oct 28, 2025
d99e562
Fix spell check
mhawryluk Oct 28, 2025
45f4a4d
Use FormAlertWithSubmitButton in VerifyDomainPage
mhawryluk Oct 28, 2025
f81fcd2
Create domain split navigator and first pages
mhawryluk Oct 28, 2025
d98f0de
Fixes and refactors to the verify domain flow
mhawryluk Oct 28, 2025
b24d94d
Merge branch 'feat/domain-validation' into feat/domain-page
mhawryluk Oct 28, 2025
e325f1d
Fix after merge
mhawryluk Oct 28, 2025
a2aa3d0
Rename the new modal stack navigator
mhawryluk Oct 28, 2025
dee2e6b
Divide VerifyDomain page between workspaces list/domain views
mhawryluk Oct 28, 2025
ae0b941
Change validation code loading UI + add error state
mhawryluk Oct 28, 2025
a932956
Adjust domain validate code loading and error state UI
mhawryluk Oct 29, 2025
ee45ed8
Remove unnecessary (for now) translation key
mhawryluk Oct 29, 2025
ae41ec6
Fix backend integration in the verify domain flow
mhawryluk Oct 29, 2025
e1fa943
Review fixes and translations
mhawryluk Oct 30, 2025
9bd80f6
Improve spanish translations
mhawryluk Oct 30, 2025
fe3b4b6
Update jsdoc
mhawryluk Oct 30, 2025
3623d08
Disable retry button when user is offline
mhawryluk Oct 30, 2025
e1648fd
Move status string literals to constant
mhawryluk Oct 30, 2025
14801f8
Change Domain error status handling
mhawryluk Oct 31, 2025
fc1b7f7
Show dot indicator conditionally
mhawryluk Oct 31, 2025
23e7e50
FIx word overflow bug in VerifyDomain page on mobile
mhawryluk Oct 31, 2025
fc7aa0a
Add bottom padding to DomainVerifiedPage
mhawryluk Oct 31, 2025
e5e76d1
Merge branch 'main' into feat/domain-validation
mhawryluk Oct 31, 2025
d4a2fd0
Merge branch 'main' into feat/domain-validation
mhawryluk Oct 31, 2025
8e3ad31
Improve DomainsListRow styling
mhawryluk Oct 31, 2025
eaa6c9b
Tiny fix
mhawryluk Oct 31, 2025
aed85ad
Remove unnecessary optional chaining
mhawryluk Oct 31, 2025
9b72d87
Review fixes
mhawryluk Nov 3, 2025
a6c396c
Show validateCode error directly from onyx
mhawryluk Nov 3, 2025
ec90ad0
Merge branch 'main' into feat/domain-validation
mhawryluk Nov 3, 2025
d3677b4
Align workspaces and domain row icons
mhawryluk Nov 3, 2025
92a7c6a
Implement AI review suggestions
mhawryluk Nov 3, 2025
f2f845b
Merge branch 'feat/domain-validation' into feat/domain-page
mhawryluk Nov 3, 2025
a9f4dad
Fix after merge
mhawryluk Nov 3, 2025
cbfd0be
Compress svg
mhawryluk Nov 3, 2025
29a5631
Make domain verify RHPs appear above domain page after refresh
mhawryluk Nov 3, 2025
6da0eb6
Fix header back button navigation from domain center pages
mhawryluk Nov 3, 2025
93b0776
Separate DomainVerifiedPage between workspaces list/domain page
mhawryluk Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/images/laptop-on-desk-with-coffee-and-key.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,8 @@
"mple",
"Selec",
"setuptools",
"DYNAMICEXTERNAL"
"DYNAMICEXTERNAL",
"Wooo"
],
"ignorePaths": [
"src/languages/de.ts",
Expand Down
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,8 @@ const CONST = {
PLAN_TYPES_AND_PRICING_HELP_URL: 'https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing',
MERGE_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/settings/Merge-Accounts',
CONNECT_A_BUSINESS_BANK_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account',
DOMAIN_VERIFICATION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain',
SAML_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify',
REGISTER_FOR_WEBINAR_URL: 'https://events.zoom.us/eo/Aif1I8qCi1GZ7KnLnd1vwGPmeukSRoPjFpyFAZ2udQWn0-B86e1Z~AggLXsr32QYFjq8BlYLZ5I06Dg',
TEST_RECEIPT_URL: `${CLOUDFRONT_URL}/images/fake-receipt__tacotodds.png`,
// Use Environment.getEnvironmentURL to get the complete URL with port number
Expand Down
1 change: 1 addition & 0 deletions src/NAVIGATORS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
REPORTS_SPLIT_NAVIGATOR: 'ReportsSplitNavigator',
SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator',
WORKSPACE_SPLIT_NAVIGATOR: 'WorkspaceSplitNavigator',
DOMAIN_SPLIT_NAVIGATOR: 'DomainSplitNavigator',
SEARCH_FULLSCREEN_NAVIGATOR: 'SearchFullscreenNavigator',
SHARE_MODAL_NAVIGATOR: 'ShareModalNavigator',
PUBLIC_RIGHT_MODAL_NAVIGATOR: 'PublicRightModalNavigator',
Expand Down
24 changes: 24 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3302,6 +3302,30 @@ const ROUTES = {
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
getRoute: (backTo?: string) => getUrlWithBackToParam('test-tools' as const, backTo),
},
WORKSPACES_VERIFY_DOMAIN: {
route: 'workspaces/verify-domain/:accountID',
getRoute: (accountID: number) => `workspaces/verify-domain/${accountID}` as const,
},
WORKSPACES_DOMAIN_VERIFIED: {
route: 'workspaces/domain-verified/:accountID',
getRoute: (accountID: number) => `workspaces/domain-verified/${accountID}` as const,
},
DOMAIN_INITIAL: {
route: 'domain/:accountID',
getRoute: (accountID: number) => `domain/${accountID}` as const,
},
DOMAIN_SAML: {
route: 'domain/:accountID/saml',
getRoute: (accountID: number) => `domain/${accountID}/saml` as const,
},
DOMAIN_VERIFY: {
route: 'domain/:accountID/verify',
getRoute: (accountID: number) => `domain/${accountID}/verify` as const,
},
DOMAIN_VERIFIED: {
route: 'domain/:accountID/verified',
getRoute: (accountID: number) => `domain/${accountID}/verified` as const,
},
} as const;

/**
Expand Down
9 changes: 9 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ const SCREENS = {
SCHEDULE_CALL: 'ScheduleCall',
REPORT_CHANGE_APPROVER: 'Report_Change_Approver',
MERGE_TRANSACTION: 'MergeTransaction',
DOMAIN: 'Domain',
},
PUBLIC_CONSOLE_DEBUG: 'Console_Debug',
SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop',
Expand Down Expand Up @@ -813,6 +814,14 @@ const SCREENS = {
TEST_TOOLS_MODAL: {
ROOT: 'TestToolsModal_Root',
},
WORKSPACES_VERIFY_DOMAIN: 'Workspaces_Verify_Domain',
WORKSPACES_DOMAIN_VERIFIED: 'Workspaces_Domain_Verified',
DOMAIN: {
VERIFY: 'Domain_Verify',
VERIFIED: 'Domain_Verified',
INITIAL: 'Domain_Initial',
SAML: 'Domain_SAML',
},
} as const;

type Screen = DeepValueOf<typeof SCREENS>;
Expand Down
41 changes: 41 additions & 0 deletions src/components/Domain/CopyableTextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import {View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import CopyTextToClipboard from '@components/CopyTextToClipboard';
import Text from '@components/Text';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

type CopyableTextFieldProps = {
/** Text to display and to copy */
value?: string;

/** Should an activity indicator be shown instead of the text and button */
isLoading?: boolean;
};

const ACTIVITY_INDICATOR_SIZE = 24;

function CopyableTextField({value, isLoading = false}: CopyableTextFieldProps) {
const styles = useThemeStyles();
const theme = useTheme();
return (
<View style={[styles.qbdSetupLinkBox, styles.border, styles.flexRow, styles.gap2, styles.justifyContentCenter, styles.alignItemsCenter]}>
{isLoading ? (
<ActivityIndicator
color={theme.text}
size={ACTIVITY_INDICATOR_SIZE}
/>
) : (
<>
<Text style={styles.copyableTextField}>{value ?? ''}</Text>
<View style={[styles.reportActionContextMenuMiniButton, styles.overflowHidden, styles.buttonHoveredBG]}>
<CopyTextToClipboard urlToCopy={value ?? ''} />
</View>
</>
)}
</View>
);
}

export default CopyableTextField;
81 changes: 81 additions & 0 deletions src/components/Domain/DomainMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import * as Expensicons from '@components/Icon/Expensicons';
import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import {PressableWithoutFeedback} from '@components/Pressable';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import DomainsListRow from './DomainsListRow';

type DomainMenuItemProps = {item: DomainItem; index: number};

type DomainItem = {
listItemType: 'domain';

/** main text to show in the row */
title: string;

/** function to run when clicking on the row */
action: () => void;

/** id of the row's domain */
accountID: number;

/** whether the user is an admin of the row's domain */
isAdmin: boolean;

/** whether the row's domain is validated (aka verified) */
isValidated: boolean;
} & Pick<OfflineWithFeedbackProps, 'pendingAction'>;

function DomainMenuItem({item, index}: DomainMenuItemProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const {isAdmin, isValidated} = item;

const threeDotsMenuItems: PopoverMenuItem[] | undefined =
!isValidated && isAdmin
? [
{
icon: Expensicons.Globe,
text: translate('domain.verifyDomain.title'),
onSelected: () => Navigation.navigate(ROUTES.WORKSPACES_VERIFY_DOMAIN.getRoute(item.accountID)),
},
]
: undefined;

return (
<OfflineWithFeedback
key={`domain_${item.title}_${index}`}
pendingAction={item.pendingAction}
style={styles.mb2}
>
<PressableWithoutFeedback
role={CONST.ROLE.BUTTON}
accessibilityLabel="row"
style={styles.mh5}
onPress={item.action}
disabled={!isAdmin}
>
{({hovered}) => (
<DomainsListRow
title={item.title}
badgeText={isAdmin && !isValidated ? translate('domain.notVerified') : undefined}
isHovered={hovered}
menuItems={threeDotsMenuItems}
/>
)}
</PressableWithoutFeedback>
</OfflineWithFeedback>
);
}

DomainMenuItem.displayName = 'DomainMenuItem';

export type {DomainItem};
export default DomainMenuItem;
58 changes: 50 additions & 8 deletions src/components/Domain/DomainsListRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import Badge from '@components/Badge';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import TextWithTooltip from '@components/TextWithTooltip';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';

type DomainsListRowProps = {
/** Name of the domain */
Expand All @@ -13,17 +18,23 @@ type DomainsListRowProps = {
/** Whether the row is hovered, so we can modify its style */
isHovered: boolean;

/** Whether the icon at the end of the row should be displayed */
shouldShowRightIcon: boolean;
/** The text to display inside a badge next to the title */
badgeText?: string;

/** Items for the three dots menu */
menuItems?: PopoverMenuItem[];

/** The type of brick road indicator to show. */
brickRoadIndicator?: ValueOf<typeof CONST.BRICK_ROAD_INDICATOR_STATUS>;
};

function DomainsListRow({title, isHovered, shouldShowRightIcon}: DomainsListRowProps) {
function DomainsListRow({title, isHovered, badgeText, brickRoadIndicator, menuItems}: DomainsListRowProps) {
const styles = useThemeStyles();
const theme = useTheme();

return (
<View style={[styles.flexRow, styles.highlightBG, styles.br3, styles.p5, styles.alignItemsCenter, styles.gap3, isHovered && styles.hoveredComponentBG]}>
<View style={[styles.flex1, styles.flexRow, styles.bgTransparent, styles.gap3, styles.alignItemsCenter]}>
<View style={[styles.flexRow, styles.highlightBG, styles.br3, styles.p5, styles.pr3, styles.alignItemsCenter, styles.gap3, isHovered && styles.hoveredComponentBG]}>
<View style={[styles.flex1, styles.flexRow, styles.bgTransparent, styles.gap3, styles.alignItemsCenter, styles.justifyContentStart]}>
<Icon
src={Expensicons.Globe}
fill={theme.icon}
Expand All @@ -32,19 +43,50 @@ function DomainsListRow({title, isHovered, shouldShowRightIcon}: DomainsListRowP
<TextWithTooltip
text={title}
shouldShowTooltip
style={[styles.textStrong]}
style={styles.textStrong}
/>

{!!badgeText && (
<View style={[styles.flexRow, styles.gap2, styles.alignItemsCenter, styles.justifyContentEnd]}>
<Badge
text={badgeText}
textStyles={styles.textStrong}
badgeStyles={[styles.alignSelfCenter, styles.badgeBordered]}
/>
</View>
)}
</View>

{shouldShowRightIcon && (
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<View style={[styles.flexRow, styles.justifyContentEnd]}>
<View style={[styles.flexRow, styles.ml2, styles.alignItemsCenter]}>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.workspaceListRBR, styles.pr3, styles.mt0]}>
{!!brickRoadIndicator && (
<Icon
src={Expensicons.DotIndicator}
fill={brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR ? theme.danger : theme.iconSuccessFill}
/>
)}
</View>
{!!menuItems?.length && (
<ThreeDotsMenu
shouldSelfPosition
menuItems={menuItems}
anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
shouldOverlay
isNested
/>
)}
</View>
</View>
<View style={styles.touchableButtonImage}>
<Icon
src={Expensicons.NewWindow}
fill={isHovered ? theme.iconHovered : theme.icon}
isButtonIcon
/>
</View>
)}
</View>
</View>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/FeatureList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type FeatureListProps = {
/** The text to display in the subtitle of the section */
subtitle?: string;

/** The component to display custom subtitle */
renderSubtitle?: () => ReactNode;

/** Text of the call to action button */
ctaText?: string;

Expand Down Expand Up @@ -76,6 +79,7 @@ function FeatureList({
contentPaddingOnLargeScreens,
footer,
isButtonDisabled = false,
renderSubtitle,
}: FeatureListProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
Expand All @@ -92,6 +96,7 @@ function FeatureList({
titleStyles={titleStyles}
illustrationContainerStyle={illustrationContainerStyle}
contentPaddingOnLargeScreens={contentPaddingOnLargeScreens}
renderSubtitle={renderSubtitle}
>
<View style={styles.flex1}>
<View style={[styles.flex1, styles.flexRow, styles.flexWrap, styles.rowGap4, styles.pv4, styles.pl1]}>
Expand Down
4 changes: 4 additions & 0 deletions src/components/Icon/Illustrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import CompanyCardsPendingState from '@assets/images/companyCards/pendingstate_l
import Computer from '@assets/images/computer.svg';
import EmptyCardState from '@assets/images/emptystate__expensifycard.svg';
import ExpensifyCardImage from '@assets/images/expensify-card.svg';
import LaptopOnDeskWithCoffeeAndKey from '@assets/images/laptop-on-desk-with-coffee-and-key.svg';
import LaptopWithSecondScreenAndHourglass from '@assets/images/laptop-with-second-screen-and-hourglass.svg';
import LaptopWithSecondScreenSync from '@assets/images/laptop-with-second-screen-sync.svg';
import LaptopWithSecondScreenX from '@assets/images/laptop-with-second-screen-x.svg';
Expand Down Expand Up @@ -84,6 +85,7 @@ import Luggage from '@assets/images/simple-illustrations/simple-illustration__lu
import Mailbox from '@assets/images/simple-illustrations/simple-illustration__mailbox.svg';
import ExpensifyMobileApp from '@assets/images/simple-illustrations/simple-illustration__mobileapp.svg';
import MoneyIntoWallet from '@assets/images/simple-illustrations/simple-illustration__moneyintowallet.svg';
import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__opensafe.svg';
import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg';
import PaperAirplane from '@assets/images/simple-illustrations/simple-illustration__paperairplane.svg';
import Pencil from '@assets/images/simple-illustrations/simple-illustration__pencil.svg';
Expand Down Expand Up @@ -224,4 +226,6 @@ export {
PaperAirplane,
CardReplacementSuccess,
EmptyShelves,
OpenSafe,
LaptopOnDeskWithCoffeeAndKey,
};
Loading
Loading