Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/quiet-survey-languages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-react-native': patch
---

Update displayed surveys when the person's language changes, preserving in-progress answers and keeping survey event language metadata in sync.
7 changes: 6 additions & 1 deletion packages/react-native/src/posthog-rn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,12 @@ export class PostHog extends PostHogCore {

setPersistedProperty<T>(key: PostHogPersistedProperty, value: T | null): void {
const storage = this._storageForKey(key)
return value !== null ? storage.setItem(key, value) : storage.removeItem(key)
value !== null ? storage.setItem(key, value) : storage.removeItem(key)
if (key === PostHogPersistedProperty.PersonProperties) {
// Notify surveys after the in-memory write, including unsets and resets,
// without waiting for a feature flag reload.
this._events.emit('personProperties', value)
}
}

/**
Expand Down
23 changes: 19 additions & 4 deletions packages/react-native/src/surveys/PostHogSurveyProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Survey, SurveyAppearance, SurveyType, type SurveyResponses } from '@pos
import { usePostHog } from '../hooks/usePostHog'
import { useFeatureFlags } from '../hooks/useFeatureFlags'
import { PostHog } from '../posthog-rn'
import { applySurveyTranslationForUser } from './survey-translations'
import { applySurveyTranslationForUser, detectUserLanguage } from './survey-translations'

type ActiveSurveyContextType =
| {
Expand Down Expand Up @@ -106,13 +106,24 @@ export function PostHogSurveyProvider(props: PostHogSurveyProviderProps): JSX.El
const activatedSurveys = useActivatedSurveys(posthog, surveys)

const flags = useFeatureFlags(posthog)
const [userLanguage, setUserLanguage] = useState(() => detectUserLanguage(posthog))

useEffect(() => {
const updateLanguage = () => setUserLanguage(detectUserLanguage(posthog))
const unsubscribe = posthog.on('personProperties', updateLanguage)
updateLanguage()
return unsubscribe
}, [posthog])

// Load surveys once
useEffect(() => {
posthog
.ready()
.then(() => posthog._onSurveysReady())
.then(() => posthog.getSurveys())
.then(() => {
setUserLanguage(detectUserLanguage(posthog))
return posthog.getSurveys()
})
.then(setSurveys)
.catch(() => {})
}, [posthog])
Expand Down Expand Up @@ -146,8 +157,8 @@ export function PostHogSurveyProvider(props: PostHogSurveyProviderProps): JSX.El
}, [activeSurvey, flags, surveys, seenSurveys, activatedSurveys])

const translatedActiveSurvey = useMemo(() => {
return activeSurvey ? applySurveyTranslationForUser(activeSurvey, posthog) : undefined
}, [activeSurvey, posthog])
return activeSurvey ? applySurveyTranslationForUser(activeSurvey, posthog, userLanguage) : undefined
}, [activeSurvey, posthog, userLanguage])

// Merge survey appearance so that components and hooks can use a consistent model
const surveyAppearance = useMemo<SurveyAppearanceTheme>(() => {
Expand Down Expand Up @@ -180,6 +191,10 @@ export function PostHogSurveyProvider(props: PostHogSurveyProviderProps): JSX.El
survey: translatedActiveSurvey.survey,
surveyLanguage: translatedActiveSurvey.language,
onShow: () => {
// Updating translated copy changes this callback, but does not show a new survey.
if (shownSurveyIdRef.current === activeSurvey.id) {
return
}
shownSurveyIdRef.current = activeSurvey.id
sendSurveyShownEvent(translatedActiveSurvey.survey, posthog, translatedActiveSurvey.language)
setLastSeenSurveyDate(new Date())
Expand Down
31 changes: 18 additions & 13 deletions packages/react-native/src/surveys/components/QuestionTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,10 @@ export function MultipleChoiceQuestion({
question = question as MultipleSurveyQuestion
const isSingleChoice = question.type === SurveyQuestionType.SingleChoice
const allowMultiple = question.type === SurveyQuestionType.MultipleChoice
const openChoice = question.hasOpenChoice ? question.choices[question.choices.length - 1] : null
const openChoiceIndex = question.hasOpenChoice ? question.choices.length - 1 : -1
const choices = useMemo(() => getDisplayOrderChoices(question as MultipleSurveyQuestion), [question])
const [selectedChoices, setSelectedChoices] = useState<string[]>([])
// Choice labels change with survey translations; keep selection tied to the original order.
const [selectedChoiceIndices, setSelectedChoiceIndices] = useState<number[]>([])
const [openEndedInput, setOpenEndedInput] = useState('')

// Only skip submit for single-choice questions without open choice
Expand All @@ -345,12 +346,14 @@ export function MultipleChoiceQuestion({
text={question.buttonText ?? appearance.submitButtonText}
submitDisabled={
!question.optional &&
(selectedChoices.length === 0 ||
(openChoice !== null && selectedChoices.includes(openChoice) && openEndedInput.length === 0))
(selectedChoiceIndices.length === 0 ||
(selectedChoiceIndices.includes(openChoiceIndex) && openEndedInput.length === 0))
}
appearance={appearance}
onSubmit={() => {
const result = selectedChoices.map((c) => (c === openChoice ? openEndedInput : c))
const result = selectedChoiceIndices.map((index) =>
index === openChoiceIndex ? openEndedInput : question.choices[index]
)
onSubmit(allowMultiple ? result : result[0])
}}
skipSubmitButton={shouldSkipSubmit}
Expand All @@ -364,27 +367,29 @@ export function MultipleChoiceQuestion({
appearance={appearance}
/>
<View style={styles.multipleChoiceOptions}>
{choices.map((choice: string, idx: number) => {
const isOpenChoice = choice === openChoice
const isSelected = selectedChoices.includes(choice)
{choices.map((choice: string, choiceIndex: number) => {
const isOpenChoice = choiceIndex === openChoiceIndex
const isSelected = selectedChoiceIndices.includes(choiceIndex)

const choiceTextColor = appearance.inputTextColor ?? getContrastingTextColor(appearance.inputBackground)

return (
<Pressable
key={idx}
key={choiceIndex}
style={[
styles.choiceOption,
{ backgroundColor: appearance.inputBackground },
isSelected ? { borderColor: getContrastingTextColor(appearance.backgroundColor) } : {},
]}
onPress={() => {
if (allowMultiple) {
setSelectedChoices(
isSelected ? selectedChoices.filter((c) => c !== choice) : [...selectedChoices, choice]
setSelectedChoiceIndices(
isSelected
? selectedChoiceIndices.filter((index) => index !== choiceIndex)
: [...selectedChoiceIndices, choiceIndex]
)
} else {
setSelectedChoices([choice])
setSelectedChoiceIndices([choiceIndex])
if (shouldSkipSubmit && !isOpenChoice) {
onSubmit(choice)
}
Expand All @@ -404,7 +409,7 @@ export function MultipleChoiceQuestion({
onChangeText={(userValue) => {
setOpenEndedInput(userValue)
if (!isSelected) {
setSelectedChoices(allowMultiple ? [...selectedChoices, choice] : [choice])
setSelectedChoiceIndices(allowMultiple ? [...selectedChoiceIndices, choiceIndex] : [choiceIndex])
}
}}
/>
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/src/surveys/survey-translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export function detectUserLanguage(instance: PostHog): string | null {

export function applySurveyTranslationForUser(
survey: Survey,
instance: PostHog
instance: PostHog,
userLanguage = detectUserLanguage(instance)
): { survey: Survey; language: string | null } {
const userLanguage = detectUserLanguage(instance)
const logger = getLogger(instance)

if (!userLanguage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ vi.mock('../src/surveys/components/Surveys', () => ({

// Skip translation resolution — irrelevant to presentation gating.
vi.mock('../src/surveys/survey-translations', () => ({
detectUserLanguage: () => null,
applySurveyTranslationForUser: (survey: Survey) => ({ survey, language: null }),
}))

Expand Down
Loading