Skip to content

Commit de91db7

Browse files
authored
Add survey support for React Native (#333)
1 parent 05f29fc commit de91db7

23 files changed

+1902
-1
lines changed

posthog-core/src/index.ts

+24
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * as utils from './utils'
2323
import { LZString } from './lz-string'
2424
import { SimpleEventEmitter } from './eventemitter'
2525
import { uuidv7 } from './vendor/uuidv7'
26+
import { Survey, SurveyResponse } from './posthog-surveys-types'
2627

2728
class PostHogFetchHttpError extends Error {
2829
name = 'PostHogFetchHttpError'
@@ -472,6 +473,29 @@ export abstract class PostHogCoreStateless {
472473
}
473474
}
474475

476+
/***
477+
*** SURVEYS
478+
***/
479+
480+
public async getSurveys(): Promise<Survey[]> {
481+
await this._initPromise
482+
483+
const url = `${this.host}/api/surveys/?token=${this.apiKey}`
484+
const fetchOptions: PostHogFetchOptions = {
485+
method: 'GET',
486+
headers: this.getCustomHeaders(),
487+
}
488+
489+
const response = await this.fetchWithRetry(url, fetchOptions)
490+
.then((response) => response.json() as Promise<SurveyResponse>)
491+
.catch((error) => {
492+
this._events.emit('error', error)
493+
return { surveys: [] }
494+
})
495+
496+
return response.surveys
497+
}
498+
475499
/***
476500
*** QUEUEING AND FLUSHING
477501
***/
+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/**
2+
* Having Survey types in types.ts was confusing tsc
3+
* and generating an invalid module.d.ts
4+
* See https://github.com/PostHog/posthog-js/issues/698
5+
*/
6+
7+
export interface SurveyAppearance {
8+
// keep in sync with frontend/src/types.ts -> SurveyAppearance
9+
backgroundColor?: string
10+
submitButtonColor?: string
11+
// deprecate submit button text eventually
12+
submitButtonText?: string
13+
submitButtonTextColor?: string
14+
ratingButtonColor?: string
15+
ratingButtonActiveColor?: string
16+
autoDisappear?: boolean
17+
displayThankYouMessage?: boolean
18+
thankYouMessageHeader?: string
19+
thankYouMessageDescription?: string
20+
thankYouMessageDescriptionContentType?: SurveyQuestionDescriptionContentType
21+
thankYouMessageCloseButtonText?: string
22+
borderColor?: string
23+
position?: 'left' | 'right' | 'center'
24+
placeholder?: string
25+
shuffleQuestions?: boolean
26+
surveyPopupDelaySeconds?: number
27+
// widget options
28+
widgetType?: 'button' | 'tab' | 'selector'
29+
widgetSelector?: string
30+
widgetLabel?: string
31+
widgetColor?: string
32+
}
33+
34+
export enum SurveyType {
35+
Popover = 'popover',
36+
API = 'api',
37+
Widget = 'widget',
38+
}
39+
40+
export type SurveyQuestion = BasicSurveyQuestion | LinkSurveyQuestion | RatingSurveyQuestion | MultipleSurveyQuestion
41+
42+
export type SurveyQuestionDescriptionContentType = 'html' | 'text'
43+
44+
interface SurveyQuestionBase {
45+
question: string
46+
description?: string | null
47+
descriptionContentType?: SurveyQuestionDescriptionContentType
48+
optional?: boolean
49+
buttonText?: string
50+
originalQuestionIndex: number
51+
branching?: NextQuestionBranching | EndBranching | ResponseBasedBranching | SpecificQuestionBranching
52+
}
53+
54+
export interface BasicSurveyQuestion extends SurveyQuestionBase {
55+
type: SurveyQuestionType.Open
56+
}
57+
58+
export interface LinkSurveyQuestion extends SurveyQuestionBase {
59+
type: SurveyQuestionType.Link
60+
link?: string | null
61+
}
62+
63+
export interface RatingSurveyQuestion extends SurveyQuestionBase {
64+
type: SurveyQuestionType.Rating
65+
display: 'number' | 'emoji'
66+
scale: 3 | 5 | 7 | 10
67+
lowerBoundLabel: string
68+
upperBoundLabel: string
69+
}
70+
71+
export interface MultipleSurveyQuestion extends SurveyQuestionBase {
72+
type: SurveyQuestionType.SingleChoice | SurveyQuestionType.MultipleChoice
73+
choices: string[]
74+
hasOpenChoice?: boolean
75+
shuffleOptions?: boolean
76+
}
77+
78+
export enum SurveyQuestionType {
79+
Open = 'open',
80+
MultipleChoice = 'multiple_choice',
81+
SingleChoice = 'single_choice',
82+
Rating = 'rating',
83+
Link = 'link',
84+
}
85+
86+
export enum SurveyQuestionBranchingType {
87+
NextQuestion = 'next_question',
88+
End = 'end',
89+
ResponseBased = 'response_based',
90+
SpecificQuestion = 'specific_question',
91+
}
92+
93+
interface NextQuestionBranching {
94+
type: SurveyQuestionBranchingType.NextQuestion
95+
}
96+
97+
interface EndBranching {
98+
type: SurveyQuestionBranchingType.End
99+
}
100+
101+
interface ResponseBasedBranching {
102+
type: SurveyQuestionBranchingType.ResponseBased
103+
responseValues: Record<string, any>
104+
}
105+
106+
interface SpecificQuestionBranching {
107+
type: SurveyQuestionBranchingType.SpecificQuestion
108+
index: number
109+
}
110+
111+
export interface SurveyResponse {
112+
surveys: Survey[]
113+
}
114+
115+
export type SurveyCallback = (surveys: Survey[]) => void
116+
117+
export type SurveyUrlMatchType = 'regex' | 'not_regex' | 'exact' | 'is_not' | 'icontains' | 'not_icontains'
118+
119+
export interface SurveyElement {
120+
text?: string
121+
$el_text?: string
122+
tag_name?: string
123+
href?: string
124+
attr_id?: string
125+
attr_class?: string[]
126+
nth_child?: number
127+
nth_of_type?: number
128+
attributes?: Record<string, any>
129+
event_id?: number
130+
order?: number
131+
group_id?: number
132+
}
133+
export interface SurveyRenderReason {
134+
visible: boolean
135+
disabledReason?: string
136+
}
137+
138+
export interface Survey {
139+
// Sync this with the backend's SurveyAPISerializer!
140+
id: string
141+
name: string
142+
description: string
143+
type: SurveyType
144+
feature_flag_keys:
145+
| {
146+
key: string
147+
value?: string
148+
}[]
149+
| null
150+
linked_flag_key: string | null
151+
targeting_flag_key: string | null
152+
internal_targeting_flag_key: string | null
153+
questions: SurveyQuestion[]
154+
appearance: SurveyAppearance | null
155+
conditions: {
156+
url?: string
157+
selector?: string
158+
seenSurveyWaitPeriodInDays?: number
159+
urlMatchType?: SurveyUrlMatchType
160+
events: {
161+
repeatedActivation?: boolean
162+
values: {
163+
name: string
164+
}[]
165+
} | null
166+
actions: {
167+
values: SurveyActionType[]
168+
} | null
169+
} | null
170+
start_date: string | null
171+
end_date: string | null
172+
current_iteration: number | null
173+
current_iteration_start_date: string | null
174+
}
175+
176+
export interface SurveyActionType {
177+
id: number
178+
name: string | null
179+
steps?: ActionStepType[]
180+
}
181+
182+
/** Sync with plugin-server/src/types.ts */
183+
export type ActionStepStringMatching = 'contains' | 'exact' | 'regex'
184+
185+
export interface ActionStepType {
186+
event?: string | null
187+
selector?: string | null
188+
/** @deprecated Only `selector` should be used now. */
189+
tag_name?: string
190+
text?: string | null
191+
/** @default StringMatching.Exact */
192+
text_matching?: ActionStepStringMatching | null
193+
href?: string | null
194+
/** @default ActionStepStringMatching.Exact */
195+
href_matching?: ActionStepStringMatching | null
196+
url?: string | null
197+
/** @default StringMatching.Contains */
198+
url_matching?: ActionStepStringMatching | null
199+
}

posthog-core/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export enum PostHogPersistedProperty {
6262
InstalledAppVersion = 'installed_app_version', // only used by posthog-react-native
6363
SessionReplay = 'session_replay', // only used by posthog-react-native
6464
DecideEndpointWasHit = 'decide_endpoint_was_hit', // only used by posthog-react-native
65+
SurveyLastSeenDate = 'survey_last_seen_date', // only used by posthog-react-native
66+
SurveysSeen = 'surveys_seen', // only used by posthog-react-native
6567
}
6668

6769
export type PostHogFetchOptions = {

posthog-react-native/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './src/hooks/useFeatureFlag'
99
export * from './src/hooks/usePostHog'
1010
export * from './src/PostHogProvider'
1111
export * from './src/types'
12+
export * from './src/surveys'

posthog-react-native/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"react-native": "^0.69.1",
3434
"react-native-device-info": "^10.3.0",
3535
"react-native-navigation": "^6.0.0",
36+
"react-native-safe-area-context": ">= 4.10.1",
37+
"react-native-svg": "^15.2.0",
3638
"react-native-localize": "^3.0.0",
3739
"posthog-react-native-session-replay": "^1.0.0"
3840
},
@@ -45,6 +47,8 @@
4547
"expo-localization": ">= 11.0.0",
4648
"react-native-device-info": ">= 10.0.0",
4749
"react-native-navigation": ">=6.0.0",
50+
"react-native-safe-area-context": ">= 4.10.1",
51+
"react-native-svg": ">= 15.2.0",
4852
"posthog-react-native-session-replay": "^1.0.0"
4953
},
5054
"peerDependenciesMeta": {
@@ -72,7 +76,7 @@
7276
"react-native-navigation": {
7377
"optional": true
7478
},
75-
"posthog-react-native-session-replay": {
79+
"react-native-safe-area-context": {
7680
"optional": true
7781
}
7882
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type RNSafeAreaContext from 'react-native-safe-area-context'
2+
3+
let OptionalRNSafeArea: typeof RNSafeAreaContext | undefined = undefined
4+
5+
try {
6+
// eslint-disable-next-line @typescript-eslint/no-var-requires
7+
OptionalRNSafeArea = require('react-native-safe-area-context')
8+
} catch (e) {}
9+
10+
function createDefaultInsets(): RNSafeAreaContext.EdgeInsets {
11+
// If the library isn't available, fall back to a default which should cover most devices
12+
return { top: 60, bottom: 30, left: 0, right: 0 }
13+
}
14+
15+
export const useOptionalSafeAreaInsets = (): RNSafeAreaContext.EdgeInsets => {
16+
const useSafeAreaInsets = OptionalRNSafeArea?.useSafeAreaInsets ?? createDefaultInsets
17+
try {
18+
return useSafeAreaInsets()
19+
} catch (err) {
20+
return createDefaultInsets()
21+
}
22+
}

posthog-react-native/src/posthog-rn.ts

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from './types'
2323
import { withReactNativeNavigation } from './frameworks/wix-navigation'
2424
import { OptionalReactNativeSessionReplay } from './optional/OptionalSessionReplay'
25+
import { Survey, SurveyResponse } from '../../posthog-core/src/posthog-surveys-types'
2526

2627
export type PostHogOptions = PostHogCoreOptions & {
2728
/** Allows you to provide the storage type. By default 'file'.

0 commit comments

Comments
 (0)