Skip to content

Commit

Permalink
Merge pull request #4473 from EdgeApp/jon/ab/create-login-buttons
Browse files Browse the repository at this point in the history
- added: Experiment config support between gui and login-ui
- added: Experiment config for "Create Account" button labels
  • Loading branch information
Jon-edge authored Sep 26, 2023
2 parents 0a19845 + 2ac7ae4 commit ef07ab9
Show file tree
Hide file tree
Showing 18 changed files with 170 additions and 86 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- added: Experiment config support between gui and login-ui
- added: Experiment config for "Create Account" and "Login" button labels
- changed: Always show gas warning when enabling tokens
- changed: Replace text 'plugins' with 'providers' in Buy/Sell
- changed: Tweak the boot background color on Android.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exports[`ChangePasswordComponent should render with loading props 1`] = `
}
}
onComplete={[Function]}
onLogEvent={[Function]}
/>
</SceneWrapper>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exports[`ChangePinComponent should render with loading props 1`] = `
}
}
onComplete={[Function]}
onLogEvent={[Function]}
/>
</SceneWrapper>
`;
6 changes: 3 additions & 3 deletions src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { logoutRequest } from '../actions/LoginActions'
import { showReEnableOtpModal } from '../actions/SettingsActions'
import { CryptoExchangeScene as CryptoExchangeSceneComponent } from '../components/scenes/CryptoExchangeScene'
import { ENV } from '../env'
import { getExperimentConfigValue } from '../experimentConfig'
import { useAsyncEffect } from '../hooks/useAsyncEffect'
import { lstrings } from '../locales/strings'
import { AddressFormScene } from '../plugins/gui/scenes/AddressFormScene'
Expand All @@ -22,7 +23,6 @@ import { RewardsCardDashboardScene as RewardsCardListSceneComponent } from '../p
import { RewardsCardWelcomeScene as RewardsCardWelcomeSceneComponent } from '../plugins/gui/scenes/RewardsCardWelcomeScene'
import { SepaFormScene } from '../plugins/gui/scenes/SepaFormScene'
import { defaultAccount } from '../reducers/CoreReducer'
import { getStickyConfigValue } from '../stickyConfig'
import { useDispatch, useSelector } from '../types/reactRedux'
import { AppParamList, NavigationBase } from '../types/routerTypes'
import { logEvent } from '../util/tracking'
Expand Down Expand Up @@ -251,10 +251,10 @@ export const Main = () => {
}, 0)
}, [])

// Wait for the sticky config to initialize before rendering anything
// Wait for the experiment config to initialize before rendering anything
useAsyncEffect(async () => {
if (isMaestro()) return
setLegacyLanding((await getStickyConfigValue('legacyLanding')) === 'true')
setLegacyLanding((await getExperimentConfigValue('legacyLanding')) === 'legacyLanding')
}, [])

return legacyLanding == null ? (
Expand Down
3 changes: 2 additions & 1 deletion src/components/scenes/ChangePasswordScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as React from 'react'
import { connect } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { logActivity } from '../../util/logger'
import { logEvent } from '../../util/tracking'
import { SceneWrapper } from '../common/SceneWrapper'

interface OwnProps extends EdgeSceneProps<'changePassword'> {}
Expand All @@ -24,7 +25,7 @@ export class ChangePasswordComponent extends React.Component<Props> {
}
return (
<SceneWrapper hasTabs={false} background="theme">
<ChangePasswordScreen account={account} context={context} onComplete={handleComplete} />
<ChangePasswordScreen account={account} context={context} onComplete={handleComplete} onLogEvent={logEvent} />
</SceneWrapper>
)
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/scenes/ChangePinScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as React from 'react'
import { connect } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { logActivity } from '../../util/logger'
import { logEvent } from '../../util/tracking'
import { SceneWrapper } from '../common/SceneWrapper'

interface OwnProps extends EdgeSceneProps<'changePin'> {}
Expand All @@ -24,7 +25,7 @@ export class ChangePinComponent extends React.Component<Props> {
}
return (
<SceneWrapper hasTabs={false} background="theme">
<ChangePinScreen account={account} context={context} onComplete={handleComplete} />
<ChangePinScreen account={account} context={context} onComplete={handleComplete} onLogEvent={logEvent} />
</SceneWrapper>
)
}
Expand Down
13 changes: 9 additions & 4 deletions src/components/scenes/GettingStartedScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import slide1HeroImage from '../../assets/images/gettingStarted/slide1HeroImage.
import slide2HeroImage from '../../assets/images/gettingStarted/slide2HeroImage.png'
import slide3HeroImage from '../../assets/images/gettingStarted/slide3HeroImage.png'
import slide4HeroImage from '../../assets/images/gettingStarted/slide4HeroImage.png'
import { getExperimentConfigValue } from '../../experimentConfig'
import { useAsyncEffect } from '../../hooks/useAsyncEffect'
import { useHandler } from '../../hooks/useHandler'
import { useWatch } from '../../hooks/useWatch'
import { lstrings } from '../../locales/strings'
import { getStickyConfigValue } from '../../stickyConfig'
import { useSelector } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { ImageProp } from '../../types/Theme'
Expand Down Expand Up @@ -84,6 +84,7 @@ export const GettingStartedScene = (props: Props) => {

const [isFinalSwipeEnabled, setIsFinalSwipeEnabled] = React.useState(true)
const [createAccountType, setCreateAccountType] = React.useState<CreateAccountType>('full')
const [createAccountText, setCreateAccountText] = React.useState<string>(lstrings.create_wallet_create_account)

// An extra index is added to account for the extra initial usp slide OR to
// allow the SwipeOffsetDetector extra room for the user to swipe beyond to
Expand Down Expand Up @@ -150,8 +151,12 @@ export const GettingStartedScene = (props: Props) => {

// Initialize variant config values
useAsyncEffect(async () => {
setIsFinalSwipeEnabled((await getStickyConfigValue('swipeLastUsp')) === 'true')
setCreateAccountType(await getStickyConfigValue('createAccountType'))
setIsFinalSwipeEnabled((await getExperimentConfigValue('swipeLastUsp')) === 'true')
setCreateAccountType(await getExperimentConfigValue('createAccountType'))

const createAccTextVar = await getExperimentConfigValue('createAccountText')
if (createAccTextVar === 'signUp') setCreateAccountText(lstrings.account_sign_up)
else if (createAccTextVar === 'getStarted') setCreateAccountText(lstrings.account_get_started)
}, [])

// Redirect to login screen if device has memory of accounts
Expand Down Expand Up @@ -216,7 +221,7 @@ export const GettingStartedScene = (props: Props) => {
})}
</Sections>
<Space horizontal={2}>
<MainButton onPress={handlePressSignUp} label={lstrings.create_wallet_create_account} />
<MainButton onPress={handlePressSignUp} label={createAccountText} />
<MainButton type="escape" onPress={handlePressSignIn} label={lstrings.getting_started_button_sign_in} />
</Space>
</SectionCoverAnimated>
Expand Down
16 changes: 10 additions & 6 deletions src/components/scenes/LoginScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ import { initializeAccount, logoutRequest } from '../../actions/LoginActions'
import { serverSettingsToNotificationSettings, setDeviceSettings } from '../../actions/NotificationActions'
import { cacheStyles, Theme, useTheme } from '../../components/services/ThemeContext'
import { ENV } from '../../env'
import { ExperimentConfig, getExperimentConfig } from '../../experimentConfig'
import { useAsyncEffect } from '../../hooks/useAsyncEffect'
import { useAsyncValue } from '../../hooks/useAsyncValue'
import { useHandler } from '../../hooks/useHandler'
import { useWatch } from '../../hooks/useWatch'
import { lstrings } from '../../locales/strings'
import { getStickyConfigValue } from '../../stickyConfig'
import { config } from '../../theme/appConfig'
import { useDispatch, useSelector } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { ImageProp } from '../../types/Theme'
import { GuiTouchIdInfo } from '../../types/types'
import { trackError } from '../../util/tracking'
import { logEvent, trackError } from '../../util/tracking'
import { pickRandom } from '../../util/utils'
import { withServices } from '../hoc/withServices'
import { showHelpModal } from '../modals/HelpModal'
Expand Down Expand Up @@ -62,6 +62,7 @@ export function LoginSceneComponent(props: Props) {
const [notificationPermissionsInfo, setNotificationPermissionsInfo] = React.useState<NotificationPermissionsInfo | undefined>()
const [passwordRecoveryKey, setPasswordRecoveryKey] = React.useState<string | undefined>()
const [legacyLanding, setLegacyLanding] = React.useState<boolean | undefined>(isMaestro() ? false : undefined)
const [experimentConfig, setExperimentConfig] = React.useState<ExperimentConfig>()

const fontDescription = React.useMemo(
() => ({
Expand Down Expand Up @@ -183,13 +184,14 @@ export function LoginSceneComponent(props: Props) {
dispatch(showSendLogsModal()).catch(err => showError(err))
})

// Wait for the sticky config to initialize before rendering anything
// Wait for the experiment config to initialize before rendering anything
useAsyncEffect(async () => {
if (isMaestro()) return
setLegacyLanding((await getStickyConfigValue('legacyLanding')) === 'true')
const experimentConfig = await getExperimentConfig()
setExperimentConfig(experimentConfig)
setLegacyLanding(experimentConfig.legacyLanding === 'legacyLanding')
}, [])

return loggedIn ? (
return loggedIn || experimentConfig == null ? (
<LoadingScene />
) : (
<View style={styles.container} testID="edge: login-scene">
Expand All @@ -209,7 +211,9 @@ export function LoginSceneComponent(props: Props) {
primaryLogoCallback={handleSendLogs}
recoveryLogin={passwordRecoveryKey}
skipSecurityAlerts
experimentConfig={experimentConfig}
onComplete={maybeHandleComplete}
onLogEvent={logEvent}
onLogin={handleLogin}
onNotificationPermit={setNotificationPermissionsInfo}
/>
Expand Down
10 changes: 9 additions & 1 deletion src/components/scenes/OtpRepairScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { config } from '../../theme/appConfig'
import { THEME } from '../../theme/variables/airbitz'
import { connect } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { logEvent } from '../../util/tracking'

interface OwnProps extends EdgeSceneProps<'otpRepair'> {}

Expand All @@ -24,7 +25,14 @@ class OtpRepairComponent extends React.Component<Props> {

return (
<View style={styles.container}>
<OtpRepairScreen account={account} branding={{ appName: config.appName }} context={context} onComplete={handleComplete} otpError={otpError} />
<OtpRepairScreen
account={account}
branding={{ appName: config.appName }}
context={context}
onComplete={handleComplete}
otpError={otpError}
onLogEvent={logEvent}
/>
</View>
)
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/scenes/PasswordRecoveryScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as React from 'react'
import { config } from '../../theme/appConfig'
import { connect } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { logEvent } from '../../util/tracking'
import { SceneWrapper } from '../common/SceneWrapper'

interface OwnProps extends EdgeSceneProps<'passwordRecovery'> {}
Expand All @@ -26,6 +27,7 @@ class ChangeRecoveryComponent extends React.Component<Props> {
branding={{ appName: config.appName }}
account={account}
context={context}
onLogEvent={logEvent}
onComplete={handleComplete}
// @ts-expect-error
onCancel={handleComplete}
Expand Down
3 changes: 2 additions & 1 deletion src/components/scenes/SecurityAlertsScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StatusBar, StyleSheet, View } from 'react-native'
import { THEME } from '../../theme/variables/airbitz'
import { connect } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { logEvent } from '../../util/tracking'

interface OwnProps extends EdgeSceneProps<'securityAlerts'> {}

Expand All @@ -22,7 +23,7 @@ class SecurityAlertsComponent extends React.Component<Props> {

return (
<View style={styles.container}>
<SecurityAlertsScreen account={account} context={context} onComplete={handleComplete} />
<SecurityAlertsScreen account={account} context={context} onLogEvent={logEvent} onComplete={handleComplete} />
</View>
)
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/scenes/UpgradeUsernameScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from 'react'
import { useSelector } from '../../types/reactRedux'
import { EdgeSceneProps } from '../../types/routerTypes'
import { logActivity } from '../../util/logger'
import { logEvent } from '../../util/tracking'
import { SceneWrapper } from '../common/SceneWrapper'

interface Props extends EdgeSceneProps<'upgradeUsername'> {}
Expand All @@ -19,7 +20,7 @@ export const UpgradeUsernameScene = (props: Props) => {
}
return (
<SceneWrapper hasTabs={false} background="theme">
<UpgradeUsernameScreen account={account} context={context} onComplete={handleComplete} />
<UpgradeUsernameScreen account={account} context={context} onComplete={handleComplete} onLogEvent={logEvent} />
</SceneWrapper>
)
}
2 changes: 1 addition & 1 deletion src/constants/constantSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export const FIRST_OPEN = 'firstOpen.json'
export const SCAM_WARNING = 'scamWarning.json'
export const SETTINGS_PERMISSION_LIMITS = 'SETTINGS_PERMISSION_LIMIT'
export const SETTINGS_PERMISSION_QUANTITY = 3
export const STICKY_CONFIG = 'remoteConfigSticky.json'
export const LOCAL_EXPERIMENT_CONFIG = 'remoteConfigSticky.json'
110 changes: 110 additions & 0 deletions src/experimentConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { asObject, asOptional, asValue, Cleaner } from 'cleaners'
import { makeReactNativeDisklet } from 'disklet'
import { CreateAccountType } from 'edge-login-ui-rn'
import { isMaestro } from 'react-native-is-maestro'

import { LOCAL_EXPERIMENT_CONFIG } from './constants/constantSettings'

// Persistent experiment config for A/B testing. Values initialized in this
// config persist throughout the liftetime of the app install.
export interface ExperimentConfig {
swipeLastUsp: 'true' | 'false'
createAccountType: CreateAccountType
legacyLanding: 'legacyLanding' | 'uspLanding'
createAccountText: 'signUp' | 'getStarted' | 'createAccount'
}

const experimentConfigDisklet = makeReactNativeDisklet()

// The probability (0-1) of a feature config being set to the first value(s):
// the configuration that differs from the default feature configuration.
const experimentDistribution = {
swipeLastUsp: [0.5],
createAccountType: [0.1],
legacyLanding: [0.5],
createAccountText: [0.33, 0.33]
}

/**
* Generate a random index value according to the experiment distribution to
* determine which variant gets used.
*/
const generateExperimentConfigVal = <T>(key: keyof typeof experimentDistribution, configVals: T[]): T => {
const variantProbability = experimentDistribution[key]

if (variantProbability.length !== configVals.length - 1) {
console.error(`Misconfigured experimentDistribution for: '${key}'`)
} else {
// Generate a random number between 0 and 1
const random = Math.random()

// Check which index the random number falls into and return the configVal:
let lowerBound = 0
for (let i = 0; i < variantProbability.length; i++) {
if (random >= lowerBound && random < variantProbability[i]) return configVals[i]
lowerBound += variantProbability[i]
}
}

return configVals[configVals.length - 1]
}

// It's important to define string literals instead of booleans as values so
// that they are properly captured in the analytics dashboard reports. The first
// values are the variant values that differ from the default feature
// behavior/appearance, while the last value represents unchanged
// behavior/appearance.
// If no generateCfgValFn given, returns defined defaults
// Error: Return type annotation circularly references itself.
const asExperimentConfig: (isDefault?: boolean) => Cleaner<ExperimentConfig> = isDefault =>
asObject({
swipeLastUsp: asOptional<'true' | 'false'>(asValue('true', 'false'), isDefault ? 'false' : generateExperimentConfigVal('swipeLastUsp', ['true', 'false'])),
createAccountType: asOptional<CreateAccountType>(
asValue('light', 'full'),
isDefault ? 'full' : generateExperimentConfigVal('createAccountType', ['light', 'full'])
),
legacyLanding: asOptional<'legacyLanding' | 'uspLanding'>(
asValue('legacyLanding', 'uspLanding'),
isDefault ? 'uspLanding' : generateExperimentConfigVal('legacyLanding', ['legacyLanding', 'uspLanding'])
),
createAccountText: asOptional<'signUp' | 'getStarted' | 'createAccount'>(
asValue('signUp', 'getStarted', 'createAccount'),
isDefault ? 'createAccount' : generateExperimentConfigVal('createAccountText', ['signUp', 'getStarted', 'createAccount'])
)
})

/**
* Immediately initialize the experiment config as soon as the module loads.
* This config value is available through the module's getter functions.
*/
const experimentConfigPromise: Promise<ExperimentConfig> = (async (): Promise<ExperimentConfig> => {
try {
const experimentConfigJson = await experimentConfigDisklet.getText(LOCAL_EXPERIMENT_CONFIG)
return asExperimentConfig(false)(JSON.parse(experimentConfigJson))
} catch (err) {
// Not found or incompatible. Re-generate with random values according to
// the defined distribution.
const generatedExperimentConfig = asExperimentConfig(false)({})
await experimentConfigDisklet.setText(LOCAL_EXPERIMENT_CONFIG, JSON.stringify(generatedExperimentConfig))
return generatedExperimentConfig
}
})()

/**
* Initializes the local experiment config file containing the randomly
* generated variant values. This is used for variant values that are required
* prior to the initialization of the fetched config. Once generated, values
* 'stick' until the config type changes.
*/
export const getExperimentConfig = async (): Promise<ExperimentConfig> => {
if (isMaestro()) return asExperimentConfig(true)({}) // Test with forced defaults
return await experimentConfigPromise
}

/**
* Returns the experiment config value
*/
export const getExperimentConfigValue = async <K extends keyof ExperimentConfig>(key: K): Promise<ExperimentConfig[K]> => {
const config = await getExperimentConfig()
return config[key]
}
3 changes: 3 additions & 0 deletions src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,9 @@ const strings = {
getting_started_welcome_prompt: `Swipe to Learn More`,
getting_started_welcome_title: `Welcome to\nFinancial *Freedom*`,

account_sign_up: 'Sign Up',
account_get_started: 'Get Started',

// Accessibility Hints
app_logo_hint: 'App logo',
check_icon_hint: 'Confirmed',
Expand Down
Loading

0 comments on commit ef07ab9

Please sign in to comment.