feat(mobile): integrate broadcast and survey clients
This commit is contained in:
parent
97c9eaed24
commit
48896ab879
@ -18,11 +18,13 @@
|
|||||||
"@bytelyst/api-client": "^0.1.0",
|
"@bytelyst/api-client": "^0.1.0",
|
||||||
"@bytelyst/auth-client": "^0.1.0",
|
"@bytelyst/auth-client": "^0.1.0",
|
||||||
"@bytelyst/blob-client": "^0.1.0",
|
"@bytelyst/blob-client": "^0.1.0",
|
||||||
|
"@bytelyst/broadcast-client": "^0.1.0",
|
||||||
"@bytelyst/design-tokens": "^0.1.0",
|
"@bytelyst/design-tokens": "^0.1.0",
|
||||||
"@bytelyst/diagnostics-client": "^0.1.0",
|
"@bytelyst/diagnostics-client": "^0.1.0",
|
||||||
"@bytelyst/feature-flag-client": "^0.1.0",
|
"@bytelyst/feature-flag-client": "^0.1.0",
|
||||||
"@bytelyst/kill-switch-client": "^0.1.0",
|
"@bytelyst/kill-switch-client": "^0.1.0",
|
||||||
"@bytelyst/offline-queue": "^0.1.0",
|
"@bytelyst/offline-queue": "^0.1.0",
|
||||||
|
"@bytelyst/survey-client": "^0.1.0",
|
||||||
"@bytelyst/telemetry-client": "^0.1.0",
|
"@bytelyst/telemetry-client": "^0.1.0",
|
||||||
"expo": "~55.0.4",
|
"expo": "~55.0.4",
|
||||||
"expo-router": "~6.0.4",
|
"expo-router": "~6.0.4",
|
||||||
|
|||||||
@ -1,15 +1,26 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { Text, View } from 'react-native';
|
import { Modal, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||||
import { useAuthStore, type AuthState } from '../store/auth-store';
|
import { useAuthStore, type AuthState } from '../store/auth-store';
|
||||||
import { useInboxStore, type InboxState } from '../store/inbox-store';
|
import { useInboxStore, type InboxState } from '../store/inbox-store';
|
||||||
import { useNotesStore, type NotesState } from '../store/notes-store';
|
import { useNotesStore, type NotesState } from '../store/notes-store';
|
||||||
import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store';
|
import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store';
|
||||||
import { checkKillSwitch, initPlatform } from '../lib/platform';
|
import { checkKillSwitch, initPlatform } from '../lib/platform';
|
||||||
|
import { getBroadcastClient } from '../lib/broadcast-client';
|
||||||
|
import { getSurveyClient } from '../lib/survey-client';
|
||||||
|
import type { InAppMessage } from '@bytelyst/broadcast-client';
|
||||||
|
import type { ActiveSurvey, Question, QuestionAnswer } from '@bytelyst/survey-client';
|
||||||
import { colors } from '../theme';
|
import { colors } from '../theme';
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
|
const [broadcastMessages, setBroadcastMessages] = useState<InAppMessage[]>([]);
|
||||||
|
const [activeSurvey, setActiveSurvey] = useState<ActiveSurvey | null>(null);
|
||||||
|
const [surveyStarted, setSurveyStarted] = useState(false);
|
||||||
|
const [surveyIndex, setSurveyIndex] = useState(0);
|
||||||
|
const [surveyAnswers, setSurveyAnswers] = useState<Record<string, QuestionAnswer>>({});
|
||||||
|
const [textAnswer, setTextAnswer] = useState('');
|
||||||
|
|
||||||
const [killSwitchState, setKillSwitchState] = useState<{
|
const [killSwitchState, setKillSwitchState] = useState<{
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@ -25,6 +36,38 @@ export default function RootLayout() {
|
|||||||
const hydrateNotes = useNotesStore((state: NotesState) => state.hydrate);
|
const hydrateNotes = useNotesStore((state: NotesState) => state.hydrate);
|
||||||
const hydrateWorkspaces = useWorkspaceStore((state: WorkspaceState) => state.hydrate);
|
const hydrateWorkspaces = useWorkspaceStore((state: WorkspaceState) => state.hydrate);
|
||||||
|
|
||||||
|
function toSurveyAnswer(question: Question, value: string): QuestionAnswer {
|
||||||
|
if (question.type === 'single_choice' || question.type === 'dropdown') {
|
||||||
|
return { type: 'single_choice', optionId: value };
|
||||||
|
}
|
||||||
|
if (question.type === 'multiple_choice') {
|
||||||
|
return { type: 'multiple_choice', optionIds: [value] };
|
||||||
|
}
|
||||||
|
if (question.type === 'ranking') {
|
||||||
|
return { type: 'ranking', rankedOptionIds: [value] };
|
||||||
|
}
|
||||||
|
if (question.type === 'rating' || question.type === 'nps' || question.type === 'scale') {
|
||||||
|
return { type: 'rating', value: Number(value) };
|
||||||
|
}
|
||||||
|
return { type: 'text', value };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBroadcasts(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { messages } = await getBroadcastClient().listMessages();
|
||||||
|
setBroadcastMessages(messages.filter((message) => message.status !== 'dismissed'));
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSurvey(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { survey } = await getSurveyClient().getActiveSurvey();
|
||||||
|
setActiveSurvey((current) => current ?? survey);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void checkKillSwitch()
|
void checkKillSwitch()
|
||||||
.then((state) => {
|
.then((state) => {
|
||||||
@ -43,6 +86,21 @@ export default function RootLayout() {
|
|||||||
void hydrateNotes();
|
void hydrateNotes();
|
||||||
void hydrateWorkspaces();
|
void hydrateWorkspaces();
|
||||||
void hydrateInbox();
|
void hydrateInbox();
|
||||||
|
void loadBroadcasts();
|
||||||
|
void loadSurvey();
|
||||||
|
|
||||||
|
const broadcastTimer = setInterval(() => {
|
||||||
|
void loadBroadcasts();
|
||||||
|
}, 5 * 60_000);
|
||||||
|
|
||||||
|
const surveyTimer = setInterval(() => {
|
||||||
|
void loadSurvey();
|
||||||
|
}, 10 * 60_000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(broadcastTimer);
|
||||||
|
clearInterval(surveyTimer);
|
||||||
|
};
|
||||||
}, [bootstrapAuth, hydrateInbox, hydrateNotes, hydrateWorkspaces]);
|
}, [bootstrapAuth, hydrateInbox, hydrateNotes, hydrateWorkspaces]);
|
||||||
|
|
||||||
if (killSwitchState.checked && killSwitchState.disabled) {
|
if (killSwitchState.checked && killSwitchState.disabled) {
|
||||||
@ -68,15 +126,334 @@ export default function RootLayout() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentQuestion = activeSurvey?.questions[surveyIndex];
|
||||||
|
|
||||||
|
async function dismissBroadcast(messageId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await getBroadcastClient().markDismissed(messageId);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
setBroadcastMessages((current) => current.filter((message) => message.id !== messageId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startSurvey(): Promise<void> {
|
||||||
|
if (!activeSurvey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await getSurveyClient().startSurvey(activeSurvey.id);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
setSurveyStarted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dismissSurvey(): Promise<void> {
|
||||||
|
if (activeSurvey) {
|
||||||
|
try {
|
||||||
|
await getSurveyClient().dismissSurvey(activeSurvey.id);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveSurvey(null);
|
||||||
|
setSurveyStarted(false);
|
||||||
|
setSurveyIndex(0);
|
||||||
|
setSurveyAnswers({});
|
||||||
|
setTextAnswer('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAnswer(value: string): Promise<void> {
|
||||||
|
if (!activeSurvey || !currentQuestion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answer = toSurveyAnswer(currentQuestion, value);
|
||||||
|
setSurveyAnswers((current) => ({
|
||||||
|
...current,
|
||||||
|
[currentQuestion.id]: answer,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getSurveyClient().submitAnswer(activeSurvey.id, currentQuestion.id, answer);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLastQuestion = surveyIndex >= activeSurvey.questions.length - 1;
|
||||||
|
if (isLastQuestion) {
|
||||||
|
try {
|
||||||
|
await getSurveyClient().completeSurvey(activeSurvey.id);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
await dismissSurvey();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSurveyIndex((current) => current + 1);
|
||||||
|
setTextAnswer('');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View style={styles.root}>
|
||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
<Stack
|
{broadcastMessages.length > 0 ? (
|
||||||
screenOptions={{
|
<View style={styles.bannerSection}>
|
||||||
headerShown: false,
|
{broadcastMessages.map((message) => (
|
||||||
animation: 'fade',
|
<View key={message.id} style={styles.bannerCard}>
|
||||||
}}
|
<View style={styles.bannerContent}>
|
||||||
/>
|
<Text style={styles.bannerTitle}>{message.title}</Text>
|
||||||
</>
|
{message.body ? <Text style={styles.bannerBody}>{message.body}</Text> : null}
|
||||||
|
</View>
|
||||||
|
{message.dismissible !== false ? (
|
||||||
|
<Pressable onPress={() => void dismissBroadcast(message.id)}>
|
||||||
|
<Text style={styles.bannerDismiss}>×</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{activeSurvey && !surveyStarted ? (
|
||||||
|
<View style={styles.surveyPrompt}>
|
||||||
|
<Text style={styles.surveyPromptText}>{activeSurvey.title}</Text>
|
||||||
|
<View style={styles.surveyActions}>
|
||||||
|
<Pressable style={styles.primaryButton} onPress={() => void startSurvey()}>
|
||||||
|
<Text style={styles.primaryButtonText}>Start</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.secondaryButton} onPress={() => void dismissSurvey()}>
|
||||||
|
<Text style={styles.secondaryButtonText}>Dismiss</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View style={styles.stackContainer}>
|
||||||
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
animation: 'fade',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Modal animationType="slide" transparent visible={Boolean(activeSurvey && surveyStarted && currentQuestion)}>
|
||||||
|
<View style={styles.modalBackdrop}>
|
||||||
|
<View style={styles.modalCard}>
|
||||||
|
<Text style={styles.modalTitle}>{activeSurvey?.title}</Text>
|
||||||
|
<Text style={styles.modalQuestion}>{currentQuestion?.text}</Text>
|
||||||
|
<Text style={styles.modalMeta}>
|
||||||
|
{surveyIndex + 1}/{activeSurvey?.questions.length ?? 1}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{currentQuestion?.options?.length ? (
|
||||||
|
<ScrollView contentContainerStyle={styles.optionsList}>
|
||||||
|
{currentQuestion.options.map((option) => (
|
||||||
|
<Pressable
|
||||||
|
key={option.id}
|
||||||
|
style={styles.optionButton}
|
||||||
|
onPress={() => void submitAnswer(option.id)}
|
||||||
|
>
|
||||||
|
<Text style={styles.optionText}>{option.emoji ? `${option.emoji} ` : ''}{option.text}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
) : currentQuestion && (currentQuestion.type === 'rating' || currentQuestion.type === 'nps' || currentQuestion.type === 'scale') ? (
|
||||||
|
<View style={styles.ratingRow}>
|
||||||
|
{Array.from(
|
||||||
|
{ length: (currentQuestion.maxValue ?? 5) - (currentQuestion.minValue ?? 1) + 1 },
|
||||||
|
(_, index) => (currentQuestion.minValue ?? 1) + index,
|
||||||
|
).map((value) => (
|
||||||
|
<Pressable key={value} style={styles.ratingButton} onPress={() => void submitAnswer(String(value))}>
|
||||||
|
<Text style={styles.ratingText}>{value}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
value={textAnswer}
|
||||||
|
onChangeText={setTextAnswer}
|
||||||
|
placeholder="Your answer"
|
||||||
|
placeholderTextColor={colors.textTertiary}
|
||||||
|
style={styles.answerInput}
|
||||||
|
multiline={currentQuestion?.type === 'text_long'}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.primaryButton, textAnswer.trim().length === 0 ? styles.disabledButton : null]}
|
||||||
|
disabled={textAnswer.trim().length === 0}
|
||||||
|
onPress={() => void submitAnswer(textAnswer.trim())}
|
||||||
|
>
|
||||||
|
<Text style={styles.primaryButtonText}>Next</Text>
|
||||||
|
</Pressable>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Pressable style={styles.secondaryButton} onPress={() => void dismissSurvey()}>
|
||||||
|
<Text style={styles.secondaryButtonText}>Close</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.bgCanvas,
|
||||||
|
},
|
||||||
|
bannerSection: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingTop: 10,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
bannerCard: {
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
backgroundColor: colors.bgElevated,
|
||||||
|
padding: 10,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
bannerContent: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 4,
|
||||||
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
bannerTitle: {
|
||||||
|
color: colors.textPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
bannerBody: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
bannerDismiss: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontSize: 18,
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
surveyPrompt: {
|
||||||
|
margin: 12,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
backgroundColor: colors.surfaceCard,
|
||||||
|
padding: 12,
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
surveyPromptText: {
|
||||||
|
color: colors.textPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
surveyActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: colors.accentPrimary,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
primaryButtonText: {
|
||||||
|
color: colors.textPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
secondaryButtonText: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
stackContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
modalBackdrop: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.45)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
modalCard: {
|
||||||
|
borderRadius: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
backgroundColor: colors.bgCanvas,
|
||||||
|
padding: 16,
|
||||||
|
gap: 12,
|
||||||
|
maxHeight: '85%',
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
color: colors.textPrimary,
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
modalQuestion: {
|
||||||
|
color: colors.textPrimary,
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: 21,
|
||||||
|
},
|
||||||
|
modalMeta: {
|
||||||
|
color: colors.textSecondary,
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
optionsList: {
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
optionButton: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
optionText: {
|
||||||
|
color: colors.textPrimary,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
ratingRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
ratingButton: {
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
borderRadius: 19,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
ratingText: {
|
||||||
|
color: colors.textPrimary,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
answerInput: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
backgroundColor: colors.bgElevated,
|
||||||
|
color: colors.textPrimary,
|
||||||
|
padding: 10,
|
||||||
|
minHeight: 42,
|
||||||
|
textAlignVertical: 'top' as const,
|
||||||
|
},
|
||||||
|
disabledButton: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
20
mobile/src/lib/broadcast-client.ts
Normal file
20
mobile/src/lib/broadcast-client.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { createBroadcastClient, type BroadcastClient } from '@bytelyst/broadcast-client';
|
||||||
|
import { API_CONFIG, PRODUCT_ID } from '../api/config';
|
||||||
|
import { getAuthClient } from '../api/auth';
|
||||||
|
|
||||||
|
let broadcastClient: BroadcastClient | null = null;
|
||||||
|
|
||||||
|
export function getBroadcastClient(): BroadcastClient {
|
||||||
|
if (!broadcastClient) {
|
||||||
|
broadcastClient = createBroadcastClient({
|
||||||
|
baseUrl: API_CONFIG.platformBaseUrl,
|
||||||
|
productId: PRODUCT_ID,
|
||||||
|
getAuthToken: () => getAuthClient().getAccessToken() ?? '',
|
||||||
|
platform: 'ios',
|
||||||
|
appVersion: '0.1.0',
|
||||||
|
osVersion: 'unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return broadcastClient;
|
||||||
|
}
|
||||||
20
mobile/src/lib/survey-client.ts
Normal file
20
mobile/src/lib/survey-client.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { createSurveyClient, type SurveyClient } from '@bytelyst/survey-client';
|
||||||
|
import { API_CONFIG, PRODUCT_ID } from '../api/config';
|
||||||
|
import { getAuthClient } from '../api/auth';
|
||||||
|
|
||||||
|
let surveyClient: SurveyClient | null = null;
|
||||||
|
|
||||||
|
export function getSurveyClient(): SurveyClient {
|
||||||
|
if (!surveyClient) {
|
||||||
|
surveyClient = createSurveyClient({
|
||||||
|
baseUrl: API_CONFIG.platformBaseUrl,
|
||||||
|
productId: PRODUCT_ID,
|
||||||
|
getAuthToken: () => getAuthClient().getAccessToken() ?? '',
|
||||||
|
platform: 'ios',
|
||||||
|
appVersion: '0.1.0',
|
||||||
|
osVersion: 'unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return surveyClient;
|
||||||
|
}
|
||||||
708
pnpm-lock.yaml
generated
708
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user