Skip to content

Commit

Permalink
Mostly duplicate SurveyModal for 2nd tier
Browse files Browse the repository at this point in the history
Avoiding too much refactoring because this is soon subject to change.
  • Loading branch information
Jon-edge committed Jan 9, 2025
1 parent 33b18ab commit 2dd722e
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased (develop)

- changed: `SurveyModal` expanded to a category & subcategory response
- fixed: Using deprecated wallets to handle links
- fixed: Thorchain stake minimum amount requirements

Expand Down
199 changes: 175 additions & 24 deletions src/components/modals/SurveyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,77 @@ import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from '
import { sprintf } from 'sprintf-js'

import { useHandler } from '../../hooks/useHandler'
import { getLocaleOrDefaultString } from '../../locales/intl'
import { lstrings } from '../../locales/strings'
import { config } from '../../theme/appConfig'
import { useDispatch } from '../../types/reactRedux'
import { infoServerData } from '../../util/network'
import { logEvent } from '../../util/tracking'
import { shuffleArray } from '../../util/utils'
import { ModalButtons } from '../buttons/ModalButtons'
import { EdgeCard } from '../cards/EdgeCard'
import { Airship } from '../services/AirshipInstance'
import { cacheStyles, Theme, useTheme } from '../services/ThemeContext'
import { EdgeText, HeaderText, Paragraph, SmallText } from '../themed/EdgeText'
import { SimpleTextInput } from '../themed/SimpleTextInput'
import { Radio } from '../themed/ThemedButtons'
import { EdgeModal } from './EdgeModal'

const SURVEY_OPTS = [
{ label: lstrings.survey_opt_youtube, selected: false },
{ label: lstrings.survey_opt_search_engine, selected: false },
{ label: lstrings.survey_opt_x_twitter, selected: false },
{ label: lstrings.survey_opt_in_person_event, selected: false },
{ label: lstrings.survey_opt_personal_referral, selected: false },
{ label: lstrings.survey_opt_article, selected: false }
interface SurveyCategory {
catKey: string
label: string
selected: boolean
}

const SURVEY_CATS: SurveyCategory[] = [
{ catKey: 'ads', label: lstrings.survey_opt_article, selected: false },
{ catKey: 'appStore', label: lstrings.survey_opt_app_store, selected: false },
{ catKey: 'article', label: lstrings.survey_opt_article, selected: false },
{ catKey: 'inPerson', label: lstrings.survey_opt_in_person_event, selected: false },
{ catKey: 'referral', label: lstrings.survey_opt_personal_referral, selected: false },
{ catKey: 'search', label: lstrings.survey_opt_search_engine, selected: false },
{ catKey: 'social', label: lstrings.survey_opt_youtube, selected: false }
]

interface LocaleSubcategory {
[langKey: string]: string
}

// TODO: Update info server with finalized type
const SURVEY_SUBCATS_TEMP: { [catKey: string]: LocaleSubcategory[] } = {
ads: [{ en: 'Social media ad (YouTube, X, Instagram, etc.)' }, { en: 'Website display/banner ad' }, { en: 'Search engine ad' }, { en: 'Podcast ad' }],
appStore: [{ en: 'Featured in the app store' }, { en: 'Search result in the app store' }],
article: [{ en: 'Crypto-specific blog' }, { en: 'General tech blog' }, { en: 'News outlet' }, { en: 'Influencer blog' }],
inPerson: [{ en: 'Conference/trade show' }, { en: 'Meetup' }, { en: 'Hackathon' }, { en: 'Crypto Canal', fr: 'Le Canal Crypto' }],
referral: [{ en: 'Friend or family recommendation' }, { en: 'Online forum discussion' }, { en: 'Community group (e.g., Discord, Telegram)' }],
search: [{ en: 'Google' }, { en: 'Bing' }, { en: 'DuckDuckGo' }, { en: 'Brave' }],
social: [
{ en: 'X (Twitter)' },
{ en: 'YouTube' },
{ en: 'Instagram' },
{ en: 'LinkedIn' },
{ en: 'Reddit' },
{ en: 'TikTok' },
{ en: 'Facebook' },
{ en: 'Cris Cyborg' }
]
}

export const SurveyModal = (props: { bridge: AirshipBridge<void> }) => {
const { bridge } = props
const dispatch = useDispatch()
const theme = useTheme()
const styles = getStyles(theme)

// Randomize response options, excluding "Other"
const randomizedSurveyOpts = [
...shuffleArray([...SURVEY_OPTS, ...(infoServerData.rollup?.installSurvey?.surveyOptions ?? []).map(option => ({ label: option, selected: false }))]),
{ label: lstrings.survey_opt_other_specify, selected: false }
]
const [options, setOptions] = useState(randomizedSurveyOpts)
const [options, setOptions] = useState([...shuffleArray(SURVEY_CATS), { catKey: 'other', label: lstrings.survey_opt_other_specify, selected: false }])
const [selectedIndex, setSelectedIndex] = useState<number>()

// "Other" custom text response
const [otherText, setOtherText] = useState('')
const inputHeight = useSharedValue(0)

const isOtherSelected = options[options.length - 1].selected

const handleOptionPress = useHandler((index: number) => {
setSelectedIndex(index)
setOptions(prevOptions =>
Expand All @@ -71,28 +101,149 @@ export const SurveyModal = (props: { bridge: AirshipBridge<void> }) => {
}
})

const handleSubmitPress = useHandler(async () => {
if (selectedIndex == null) return // Shouldn't happen, button is disabled if no selection
// Open the 2nd modal
const handleNextPress = useHandler(async () => {
const category = options.find(o => o.selected)?.catKey
if (category == null) return // Shouldn't happen

const isSurveyCompleted = await Airship.show((bridge: AirshipBridge<boolean>) => <SurveyModal2 bridge={bridge} category={category} />)
if (isSurveyCompleted) bridge.resolve()
})

const handleSubmitPress = useHandler(async () => {
// Only "Other" is available at this first modal
dispatch(
logEvent('Survey_Discover', {
surveyResponse: selectedIndex === options.length - 1 ? `Other: ${otherText}` : options[selectedIndex].label
logEvent('Survey_Discover2', {
surveyCategory2: 'None',
surveyResponse2: `Other: ${otherText}`
})
)

bridge.resolve()
})

const handleDismissButtonPress = useHandler(async () => {
dispatch(logEvent('Survey_Discover', { surveyResponse: 'DISMISSED' }))
const handleModalDismiss = useHandler(async () => {
dispatch(logEvent('Survey_Discover2', { surveyResponse2: 'DISMISSED' }))

bridge.resolve()
})

const handleModalDismiss = useHandler(() => {
dispatch(logEvent('Survey_Discover', { surveyResponse: 'DISMISSED' }))
const animatedStyle = useAnimatedStyle(() => ({
height: inputHeight.value,
width: '100%',
opacity: inputHeight.value ? 1 : 0
}))

bridge.resolve()
return (
<EdgeModal
bridge={bridge}
onCancel={handleModalDismiss}
title={
<View style={styles.titleContainer}>
<Paragraph center>
<HeaderText>{sprintf(lstrings.survey_discover_title_1s, config.appName)}</HeaderText>
</Paragraph>
<Paragraph center>
<SmallText>{lstrings.survey_discover_subtitle}</SmallText>
</Paragraph>
</View>
}
>
{/** HACK: iOS and Android use extraScrollHeight differently... */}
<KeyboardAwareScrollView
extraScrollHeight={Platform.OS === 'ios' ? theme.rem(-16) : theme.rem(9)}
enableOnAndroid
contentContainerStyle={styles.contentContainerStyle}
keyboardShouldPersistTaps="handled"
style={styles.containerStyle}
>
<EdgeCard>
<View style={styles.radioContainer}>
{options.map((option, index) => (
<Radio value={option.selected} onPress={() => handleOptionPress(index)} key={index}>
<EdgeText style={styles.radioLabel}>{option.label}</EdgeText>
</Radio>
))}
<Animated.View style={[styles.baseAnimatedStyle, animatedStyle]}>
<SimpleTextInput value={otherText} onChangeText={setOtherText} placeholder={lstrings.specify_placeholder} horizontalRem={0.5} bottomRem={0.5} />
</Animated.View>
</View>
</EdgeCard>
</KeyboardAwareScrollView>
<ModalButtons
primary={{
label: isOtherSelected ? lstrings.survey_opt_submit : lstrings.string_next_capitalized,
disabled: selectedIndex == null || (isOtherSelected && otherText.trim() === ''),
onPress: isOtherSelected && otherText.trim() === '' ? handleSubmitPress : handleNextPress,
spinner: false
}}
secondary={{
label: lstrings.survey_opt_dismiss,
onPress: handleModalDismiss,
spinner: false
}}
/>
</EdgeModal>
)
}

const SurveyModal2 = (props: { bridge: AirshipBridge<boolean>; category: string }) => {
const { bridge, category } = props
const dispatch = useDispatch()
const theme = useTheme()
const styles = getStyles(theme)

const [options, setOptions] = useState([
...shuffleArray(SURVEY_SUBCATS_TEMP[category] ?? []).map(o => ({ label: getLocaleOrDefaultString(o), selected: false })),
{ label: lstrings.survey_opt_other_specify, selected: false }
])
const [selectedIndex, setSelectedIndex] = useState<number>()

// "Other" custom text response
const [otherText, setOtherText] = useState('')
const inputHeight = useSharedValue(0)

const isOtherSelected = options[options.length - 1].selected

const handleOptionPress = useHandler((index: number) => {
setSelectedIndex(index)
setOptions(prevOptions =>
prevOptions.map((option, i) => ({
...option,
selected: i === index
}))
)

// TODO: Auto focus, but it's kind of buggy if using refs in this situation...
if (index === options.length - 1) {
// Handle "Other" response selection
inputHeight.value = withTiming(theme.rem(3.25), {
duration: 300,
easing: Easing.inOut(Easing.ease)
})
} else {
inputHeight.value = withTiming(0, {
duration: 300,
easing: Easing.inOut(Easing.ease)
})
}
})

const handleSubmitPress = useHandler(async () => {
if (selectedIndex == null) return // Shouldn't happen, button is disabled if no selection

dispatch(
logEvent('Survey_Discover2', {
surveyCategory2: category,
surveyResponse2: isOtherSelected ? `Other: ${otherText}` : options[selectedIndex].label
})
)

bridge.resolve(true)
})

const handleModalDismiss = useHandler(async () => {
bridge.resolve(false)
})

const animatedStyle = useAnimatedStyle(() => ({
Expand Down Expand Up @@ -140,13 +291,13 @@ export const SurveyModal = (props: { bridge: AirshipBridge<void> }) => {
<ModalButtons
primary={{
label: lstrings.survey_opt_submit,
disabled: selectedIndex == null || (selectedIndex === options.length - 1 && otherText.trim() === ''),
disabled: selectedIndex == null || (isOtherSelected && otherText.trim() === ''),
onPress: handleSubmitPress,
spinner: false
}}
secondary={{
label: lstrings.survey_opt_dismiss,
onPress: handleDismissButtonPress,
onPress: handleModalDismiss,
spinner: false
}}
/>
Expand Down
1 change: 1 addition & 0 deletions src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1616,6 +1616,7 @@ const strings = {
survey_opt_in_person_event: 'In-person Event',
survey_opt_personal_referral: 'Personal Referral',
survey_opt_article: 'Article',
survey_opt_app_store: 'App Store',
survey_opt_BTCTKVR_magazine: 'BTCTKVR Magazine',
survey_opt_submit: 'Submit',
survey_opt_dismiss: 'Dismiss',
Expand Down
4 changes: 3 additions & 1 deletion src/util/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export type TrackingEventName =
| 'Start_App_No_Accounts'
| 'Start_App_With_Accounts'
| 'Survey_Discover'
| 'Survey_Discover2'
| 'purchase'
| 'Visa_Card_Launch'
| 'Earn_Spend_Launch' // No longer used
Expand Down Expand Up @@ -136,7 +137,8 @@ export interface TrackingValues extends LoginTrackingValues {
createdWalletCurrencyCode?: string
numSelectedWallets?: number // Number of wallets to be created
numAccounts?: number // Number of full accounts saved on the device
surveyResponse?: string // User's answer to a survey
surveyCategory2?: string // User's answer to a survey (first tier response)
surveyResponse2?: string // User's answer to a survey

// Conversion values
conversionValues?: DollarConversionValues | CryptoConversionValues | SellConversionValues | BuyConversionValues | SwapConversionValues
Expand Down

0 comments on commit 2dd722e

Please sign in to comment.