Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
4 changes: 3 additions & 1 deletion src/FullstoryInitHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import ONYXKEYS from './ONYXKEYS';
*/
function FullstoryInitHandler() {
const [userMetadata] = useOnyx(ONYXKEYS.USER_METADATA);
const [session] = useOnyx(ONYXKEYS.SESSION);

useEffect(() => {
FS.init(userMetadata);
FS.init(userMetadata, session);
FS.getSessionURL()
.then((url) => {
if (!url) {
Expand All @@ -29,6 +30,7 @@ function FullstoryInitHandler() {
error: error instanceof Error ? error.message : String(error),
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userMetadata]);

return null;
Expand Down
6 changes: 3 additions & 3 deletions src/libs/Fullstory/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const FS: Fullstory = {

getChatFSClass,

init: (userMetadata) => FS.consentAndIdentify(userMetadata),
init: (userMetadata, session) => FS.consentAndIdentify(userMetadata, session),

onReady: async () => FullStory.onReady(),

Expand All @@ -25,7 +25,7 @@ const FS: Fullstory = {
FullStory.identify(String(localMetadata.accountID), localMetadata);
},

consentAndIdentify: (userMetadata) => {
consentAndIdentify: (userMetadata, session) => {
// On the first subscribe for UserMetadata, this function will be called. We need
// to confirm that we actually have any value here before proceeding.
if (!userMetadata?.accountID) {
Expand All @@ -37,7 +37,7 @@ const FS: Fullstory = {
// after the init function since this function is also called on updates for
// UserMetadata onyx key.
getEnvironment().then((envName: string) => {
if (!FS.shouldInitialize(userMetadata, envName)) {
if (!FS.shouldInitialize(userMetadata, envName, session)) {
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/libs/Fullstory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const FS: Fullstory = {
}
}),

shouldInitialize: (userMetadata, envName) => shouldInitializeFullstory(userMetadata, envName) && !isSupportAuthToken(),
shouldInitialize: (userMetadata, envName, session) => shouldInitializeFullstory(userMetadata, envName, session) && !isSupportAuthToken(session),

consent: (shouldConsent) => FullStory(CONST.FULLSTORY.OPERATION.SET_IDENTITY, {consent: shouldConsent}),

Expand All @@ -52,7 +52,7 @@ const FS: Fullstory = {
});
},

consentAndIdentify: (userMetadata) => {
consentAndIdentify: (userMetadata, session) => {
// On the first subscribe for UserMetadata, this function will be called. We need
// to confirm that we actually have any value here before proceeding.
if (!userMetadata?.accountID) {
Expand All @@ -64,7 +64,7 @@ const FS: Fullstory = {
// after the init function since this function is also called on updates for
// UserMetadata onyx key.
getEnvironment().then((envName: string) => {
if (!FS.shouldInitialize(userMetadata, envName)) {
if (!FS.shouldInitialize(userMetadata, envName, session)) {
// On web, if we started FS at some point in a browser, it will run forever. So let's shut it down if we don't want it to run.
if (isInitialized()) {
FullStory(CONST.FULLSTORY.OPERATION.SHUTDOWN);
Expand Down
8 changes: 4 additions & 4 deletions src/libs/Fullstory/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type CONST from '@src/CONST';
import type {OnyxInputOrEntry, Report, UserMetadata} from '@src/types/onyx';
import type {OnyxInputOrEntry, Report, Session, UserMetadata} from '@src/types/onyx';

import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
Expand Down Expand Up @@ -27,7 +27,7 @@ interface FSPageLikeConstructor {

type GetChatFSClass = (report: OnyxInputOrEntry<Report>) => FSClass;

type ShouldInitialize = (userMetadata: UserMetadata, envName: string) => boolean;
type ShouldInitialize = (userMetadata: UserMetadata, envName: string, session: OnyxEntry<Session>) => boolean;

type Fullstory = {
/**
Expand All @@ -43,7 +43,7 @@ type Fullstory = {
/**
* Initializes Fullstory.
*/
init: (userMetadata: OnyxEntry<UserMetadata>) => void;
init: (userMetadata: OnyxEntry<UserMetadata>, session: OnyxEntry<Session>) => void;

/**
* Executes a function when the Fullstory library is ready, either by initialization or by observing the start event.
Expand All @@ -68,7 +68,7 @@ type Fullstory = {
/**
* Initializes the Fullstory metadata with the provided metadata information.
*/
consentAndIdentify: (userMetadata: OnyxEntry<UserMetadata>) => void;
consentAndIdentify: (userMetadata: OnyxEntry<UserMetadata>, session: OnyxEntry<Session>) => void;

/**
* Sets the identity as anonymous using the Fullstory library.
Expand Down
47 changes: 27 additions & 20 deletions src/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import updateSessionAuthTokens from './updateSessionAuthTokens';

const INVALID_TOKEN = 'pizza';

let session: Session = {};
let deprecatedSession: Session = {};
let authPromiseResolver: ((value: boolean) => void) | null = null;

let isHybridAppSetupFinished = false;
Expand All @@ -92,25 +92,32 @@ let hasSwitchedAccountInHybridMode = false;
Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (value) => {
session = value ?? {};
deprecatedSession = value ?? {};

if (!session.creationDate) {
session.creationDate = new Date().getTime();
if (!deprecatedSession.creationDate) {
deprecatedSession.creationDate = new Date().getTime();
}
if (session.authToken && authPromiseResolver) {
if (deprecatedSession.authToken && authPromiseResolver) {
authPromiseResolver(true);
authPromiseResolver = null;
}
if (CONFIG.IS_HYBRID_APP && isHybridAppSetupFinished && session.authToken && !isAnonymousUser(value)) {
HybridAppModule.sendAuthToken({authToken: session.authToken});
if (CONFIG.IS_HYBRID_APP && isHybridAppSetupFinished && deprecatedSession.authToken && !isAnonymousUser(value)) {
HybridAppModule.sendAuthToken({authToken: deprecatedSession.authToken});
}
},
});

let fullstorySession: Session = {};
// Use connectWithoutView because it is only for fullstory initialization

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really love for all the full story stuff to exist in its own module. That will also help isolate the connectWithoutView() calls a bit more. Would you mind doing the small refactor? I'm not sure how much it would involve, so we could do that in a separate issue if it's too much.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot, give me plan to do it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgolen Resolved via fc0285b

Onyx.connectWithoutView({
key: ONYXKEYS.SESSION,
callback: (value) => (fullstorySession = value ?? {}),
});

// Use connectWithoutView because it is only for fullstory initialization
Onyx.connectWithoutView({
key: ONYXKEYS.USER_METADATA,
callback: Fullstory.consentAndIdentify,
callback: (value) => Fullstory.consentAndIdentify(value, fullstorySession),
});

let stashedSession: Session = {};
Expand Down Expand Up @@ -139,12 +146,12 @@ Onyx.connectWithoutView({
},
});

function isSupportAuthToken(): boolean {
return session.authTokenType === CONST.AUTH_TOKEN_TYPES.SUPPORT;
function isSupportAuthToken(session: OnyxEntry<Session> = deprecatedSession): boolean {
return session?.authTokenType === CONST.AUTH_TOKEN_TYPES.SUPPORT;
}

function isSupportalSession(): boolean {
return isSupportAuthToken() || !!session?.isSupportAuthTokenUsed;
return isSupportAuthToken() || !!deprecatedSession?.isSupportAuthTokenUsed;
}

/**
Expand Down Expand Up @@ -305,7 +312,7 @@ function callSAMLSignOut(params: LogOutParams, authToken: string): Promise<void
* Checks if the account is an anonymous account.
*/
function isAnonymousUser(sessionParam?: OnyxEntry<Session>): boolean {
return (sessionParam?.authTokenType ?? session.authTokenType) === CONST.AUTH_TOKEN_TYPES.ANONYMOUS;
return (sessionParam?.authTokenType ?? deprecatedSession.authTokenType) === CONST.AUTH_TOKEN_TYPES.ANONYMOUS;
}

function hasStashedSession(stashedSessionParam: Session | undefined, stashedCredentialsParam: Credentials | undefined): boolean {
Expand All @@ -316,7 +323,7 @@ function hasStashedSession(stashedSessionParam: Session | undefined, stashedCred
* Checks if the user has authToken
*/
function hasAuthToken(): boolean {
return !!session.authToken;
return !!deprecatedSession.authToken;
}

/**
Expand Down Expand Up @@ -378,7 +385,7 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess
// session.
const signOutPromise: Promise<void | Response<never>> =
!shouldRestoreStashedSession && !shouldStashSession
? signOut({autoGeneratedLogin: credentials?.autoGeneratedLogin, signedInWithSAML: !!session.signedInWithSAML, authToken: session.authToken})
? signOut({autoGeneratedLogin: credentials?.autoGeneratedLogin, signedInWithSAML: !!deprecatedSession.signedInWithSAML, authToken: deprecatedSession.authToken})
: Promise.resolve();

// The function redirectToSignIn will clear the whole storage, so let's create our onyx params
Expand All @@ -391,7 +398,7 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess
if (!isSupportal && shouldStashSession) {
onyxSetParams = {
[ONYXKEYS.STASHED_CREDENTIALS]: credentials,
[ONYXKEYS.STASHED_SESSION]: session,
[ONYXKEYS.STASHED_SESSION]: deprecatedSession,
};
}

Expand All @@ -413,7 +420,7 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess
newDotCurrentAccountEmail: stashedSession.email ?? '',
authToken: stashedSession.authToken ?? '',
policyID: '',
accountID: session.accountID ? String(session.accountID) : '',
accountID: deprecatedSession.accountID ? String(deprecatedSession.accountID) : '',
});
hasSwitchedAccountInHybridMode = true;
}
Expand Down Expand Up @@ -620,8 +627,8 @@ function beginSignIn(email: string) {
*/
function buildOnyxDataToCleanUpAnonymousUser(): OnyxUpdate<typeof ONYXKEYS.PERSONAL_DETAILS_LIST> {
const data: Record<string, null> = {};
if (session.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS && session.accountID) {
data[session.accountID] = null;
if (deprecatedSession.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS && deprecatedSession.accountID) {
data[deprecatedSession.accountID] = null;
}
return {
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -719,7 +726,7 @@ function setupNewDotAfterTransitionFromOldDot(hybridAppSettings: HybridAppSettin
const stashedData = hybridApp?.delegateAccessData?.isDelegateAccess
? {
[ONYXKEYS.STASHED_CREDENTIALS]: credentials,
[ONYXKEYS.STASHED_SESSION]: session,
[ONYXKEYS.STASHED_SESSION]: deprecatedSession,
}
: {
[ONYXKEYS.STASHED_CREDENTIALS]: {},
Expand Down Expand Up @@ -1446,7 +1453,7 @@ function clearTwoFactorAuthSecretKey() {
*/
function waitForUserSignIn(): Promise<boolean> {
return new Promise<boolean>((resolve) => {
if (session.authToken) {
if (deprecatedSession.authToken) {
resolve(true);
} else {
authPromiseResolver = resolve;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/InitialSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
* Return a list of menu items data for general section
* @returns object with translationKey, style and items for the general section
*/
const signOutTranslationKey = isSupportAuthToken() && hasStashedSession(stashedSession, stashedCredentials) ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut';
const signOutTranslationKey = isSupportAuthToken(session) && hasStashedSession(stashedSession, stashedCredentials) ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut';
const generalMenuItemsData: Menu = {
sectionStyle: {
...styles.pt4,
Expand Down
25 changes: 25 additions & 0 deletions tests/actions/SessionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,4 +766,29 @@ describe('Session', () => {
expect(session?.signedInWithSAML).toBe(false);
});
});

describe('isSupportAuthToken', () => {
beforeEach(() => {
jest.restoreAllMocks();
});

test('returns true when session has support authTokenType', () => {
const session: Session = {authTokenType: CONST.AUTH_TOKEN_TYPES.SUPPORT, authToken: 'token', accountID: 1, creationDate: Date.now()};
expect(SessionUtil.isSupportAuthToken(session)).toBe(true);
});

test('returns false when session has anonymous authTokenType', () => {
const session: Session = {authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS, authToken: 'token', accountID: 1, creationDate: Date.now()};
expect(SessionUtil.isSupportAuthToken(session)).toBe(false);
});

test('returns false when session has no authTokenType', () => {
const session: Session = {authToken: 'token', accountID: 1, creationDate: Date.now()};
expect(SessionUtil.isSupportAuthToken(session)).toBe(false);
});

test('returns false when session is undefined', () => {
expect(SessionUtil.isSupportAuthToken(undefined)).toBe(false);
});
});
});
56 changes: 56 additions & 0 deletions tests/unit/FullstoryTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {isSupportAuthToken} from '@libs/actions/Session';
import {shouldInitializeFullstory} from '@libs/Fullstory/common';

import CONST from '@src/CONST';
import type {Session, UserMetadata} from '@src/types/onyx';

const regularSession: Session = {authToken: 'token', accountID: 1, creationDate: Date.now()};
const supportSession: Session = {authTokenType: CONST.AUTH_TOKEN_TYPES.SUPPORT, authToken: 'supportToken', accountID: 2, creationDate: Date.now()};

describe('Fullstory', () => {
describe('shouldInitializeFullstory', () => {
const productionEnv = CONST.ENVIRONMENT.PRODUCTION;

test('returns true for production environment with non-expensify email', () => {
const metadata: UserMetadata = {accountID: 1, email: 'user@example.com'};
expect(shouldInitializeFullstory(metadata, productionEnv, regularSession)).toBe(true);
});

test('returns false for non-production environment', () => {
const metadata: UserMetadata = {accountID: 1, email: 'user@example.com'};
expect(shouldInitializeFullstory(metadata, CONST.ENVIRONMENT.DEV, regularSession)).toBe(false);
});

test('returns false for expensify partner email in production', () => {
const metadata: UserMetadata = {accountID: 1, email: 'user@expensify.com'};
expect(shouldInitializeFullstory(metadata, productionEnv, regularSession)).toBe(false);
});

test('returns true for fullstory QA test email in non-production', () => {
const metadata: UserMetadata = {accountID: 1, email: `fullstory-test${CONST.EMAIL.QA_DOMAIN}`};
expect(shouldInitializeFullstory(metadata, CONST.ENVIRONMENT.DEV, regularSession)).toBe(true);
});
});

describe('shouldInitialize with isSupportAuthToken', () => {
test('isSupportAuthToken returns true for support session', () => {
expect(isSupportAuthToken(supportSession)).toBe(true);
});

test('isSupportAuthToken returns false for regular session', () => {
expect(isSupportAuthToken(regularSession)).toBe(false);
});

test('Fullstory should not initialize when session is support token', () => {
const metadata: UserMetadata = {accountID: 1, email: 'user@example.com'};
const result = shouldInitializeFullstory(metadata, CONST.ENVIRONMENT.PRODUCTION, supportSession) && !isSupportAuthToken(supportSession);
expect(result).toBe(false);
});

test('Fullstory should initialize when session is not support token', () => {
const metadata: UserMetadata = {accountID: 1, email: 'user@example.com'};
const result = shouldInitializeFullstory(metadata, CONST.ENVIRONMENT.PRODUCTION, regularSession) && !isSupportAuthToken(regularSession);
expect(result).toBe(true);
});
});
});
Loading