use existing config system instead of local storage for showcase tracking (vibe-kanban) (#1049)
* Perfect! All tasks completed successfully. Here's what was implemented: ## Summary ✅ **Migrated showcase persistence from localStorage to v8 config system** **Backend (Rust):** - Created [v8.rs](file:///private/var/folders/5q/5vgq75y92dz0k7n62z93299r0000gn/T/vibe-kanban/worktrees/0960-use-existing-con/crates/services/src/services/config/versions/v8.rs) with `ShowcaseState` struct tracking `seen_versions: HashMap<String, u32>` - Implemented migration from v7→v8 with default empty showcases - Wired up v8 as active config version **Frontend (TypeScript):** - Created [useShowcasePersistence.ts](file:///private/var/folders/5q/5vgq75y92dz0k7n62z93299r0000gn/T/vibe-kanban/worktrees/0960-use-existing-con/frontend/src/hooks/useShowcasePersistence.ts) hook using config API - Refactored [useShowcaseTrigger.ts](file:///private/var/folders/5q/5vgq75y92dz0k7n62z93299r0000gn/T/vibe-kanban/worktrees/0960-use-existing-con/frontend/src/hooks/useShowcaseTrigger.ts) to use new persistence (kept API unchanged) - Deleted old localStorage utility - Generated TypeScript types including `ShowcaseState` **Key Features:** - Showcase state persists to config file (survives random port changes) - Tracks highest version seen per showcase ID - Optimistic updates for responsive UX - Automatic v7→v8 migration on startup * Cleanup script changes for task attempt 096017a4-28dd-4092-a3ef-7180232ee7d2 * Done! Now `default_git_branch_prefix` is imported from v7 instead of being duplicated. * Cleanup script changes for task attempt 096017a4-28dd-4092-a3ef-7180232ee7d2 * fix FeatureShowcaseModal race condition (vibe-kanban) (#1050) * Done! Collapsed v8 back into v7. The `showcases` field is now part of v7 with `#[serde(default)]`, making it backward compatible without needing a version bump. * Cleanup script changes for task attempt 096017a4-28dd-4092-a3ef-7180232ee7d2 * Fixed! ✅ * showcase simplification (vibe-kanban 94972ea4) frontend/src/config/showcases.ts I want to make it harder to make mistakes here, lets use the type system to our advantage. 1. We could forget to update the `export const showcases` variable, so instead of expecting `taskPanelShowcase`, we should move showcases to all be under one object and only access them through that object. 2. The versioning system is overkill, instead of a `showcases` map with `seen_versions` as another map, we can have a `seen_features` set (realistically an array in JSON) that we insert keys into.
This commit is contained in:
committed by
GitHub
parent
e4a4c004da
commit
50d285cfe5
@@ -60,6 +60,7 @@ fn generate_types_content() -> String {
|
|||||||
services::services::config::GitHubConfig::decl(),
|
services::services::config::GitHubConfig::decl(),
|
||||||
services::services::config::SoundFile::decl(),
|
services::services::config::SoundFile::decl(),
|
||||||
services::services::config::UiLanguage::decl(),
|
services::services::config::UiLanguage::decl(),
|
||||||
|
services::services::config::ShowcaseState::decl(),
|
||||||
services::services::auth::DeviceFlowStartResponse::decl(),
|
services::services::auth::DeviceFlowStartResponse::decl(),
|
||||||
server::routes::auth::DevicePollStatus::decl(),
|
server::routes::auth::DevicePollStatus::decl(),
|
||||||
server::routes::auth::CheckTokenResponse::decl(),
|
server::routes::auth::CheckTokenResponse::decl(),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub type SoundFile = versions::v7::SoundFile;
|
|||||||
pub type EditorType = versions::v7::EditorType;
|
pub type EditorType = versions::v7::EditorType;
|
||||||
pub type GitHubConfig = versions::v7::GitHubConfig;
|
pub type GitHubConfig = versions::v7::GitHubConfig;
|
||||||
pub type UiLanguage = versions::v7::UiLanguage;
|
pub type UiLanguage = versions::v7::UiLanguage;
|
||||||
|
pub type ShowcaseState = versions::v7::ShowcaseState;
|
||||||
|
|
||||||
/// Will always return config, trying old schemas or eventually returning default
|
/// Will always return config, trying old schemas or eventually returning default
|
||||||
pub async fn load_config_from_file(config_path: &PathBuf) -> Config {
|
pub async fn load_config_from_file(config_path: &PathBuf) -> Config {
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ fn default_git_branch_prefix() -> String {
|
|||||||
"vk".to_string()
|
"vk".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, TS, Default)]
|
||||||
|
pub struct ShowcaseState {
|
||||||
|
#[serde(default)]
|
||||||
|
pub seen_features: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, EnumString)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS, EnumString)]
|
||||||
#[ts(use_ts_enum)]
|
#[ts(use_ts_enum)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
@@ -41,6 +47,8 @@ pub struct Config {
|
|||||||
pub language: UiLanguage,
|
pub language: UiLanguage,
|
||||||
#[serde(default = "default_git_branch_prefix")]
|
#[serde(default = "default_git_branch_prefix")]
|
||||||
pub git_branch_prefix: String,
|
pub git_branch_prefix: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub showcases: ShowcaseState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -90,6 +98,7 @@ impl Config {
|
|||||||
show_release_notes: old_config.show_release_notes,
|
show_release_notes: old_config.show_release_notes,
|
||||||
language: old_config.language,
|
language: old_config.language,
|
||||||
git_branch_prefix: default_git_branch_prefix(),
|
git_branch_prefix: default_git_branch_prefix(),
|
||||||
|
showcases: ShowcaseState::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +143,7 @@ impl Default for Config {
|
|||||||
show_release_notes: false,
|
show_release_notes: false,
|
||||||
language: UiLanguage::default(),
|
language: UiLanguage::default(),
|
||||||
git_branch_prefix: default_git_branch_prefix(),
|
git_branch_prefix: default_git_branch_prefix(),
|
||||||
|
showcases: ShowcaseState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,17 +78,17 @@ export function FeatureShowcaseModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (currentStage < totalStages - 1) {
|
setCurrentStage((prev) => {
|
||||||
setCurrentStage((prev) => prev + 1);
|
if (prev >= totalStages - 1) {
|
||||||
} else {
|
onClose();
|
||||||
onClose();
|
return prev;
|
||||||
}
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevious = () => {
|
const handlePrevious = () => {
|
||||||
if (currentStage > 0) {
|
setCurrentStage((prev) => Math.max(prev - 1, 0));
|
||||||
setCurrentStage((prev) => prev - 1);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,44 +1,41 @@
|
|||||||
import { ShowcaseConfig } from '@/types/showcase';
|
import { ShowcaseConfig } from '@/types/showcase';
|
||||||
|
|
||||||
export const taskPanelShowcase: ShowcaseConfig = {
|
|
||||||
id: 'task-panel-onboarding',
|
|
||||||
version: 1,
|
|
||||||
stages: [
|
|
||||||
{
|
|
||||||
titleKey: 'showcases.taskPanel.companion.title',
|
|
||||||
descriptionKey: 'showcases.taskPanel.companion.description',
|
|
||||||
media: {
|
|
||||||
type: 'video',
|
|
||||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-companion-demo-3.mp4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: 'showcases.taskPanel.installation.title',
|
|
||||||
descriptionKey: 'showcases.taskPanel.installation.description',
|
|
||||||
media: {
|
|
||||||
type: 'video',
|
|
||||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-install-companion-3.mp4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: 'showcases.taskPanel.codeReview.title',
|
|
||||||
descriptionKey: 'showcases.taskPanel.codeReview.description',
|
|
||||||
media: {
|
|
||||||
type: 'video',
|
|
||||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-code-review-3.mp4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
titleKey: 'showcases.taskPanel.pullRequest.title',
|
|
||||||
descriptionKey: 'showcases.taskPanel.pullRequest.description',
|
|
||||||
media: {
|
|
||||||
type: 'video',
|
|
||||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-git-pr-3.mp4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const showcases = {
|
export const showcases = {
|
||||||
taskPanel: taskPanelShowcase,
|
taskPanel: {
|
||||||
};
|
id: 'task-panel-onboarding',
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
titleKey: 'showcases.taskPanel.companion.title',
|
||||||
|
descriptionKey: 'showcases.taskPanel.companion.description',
|
||||||
|
media: {
|
||||||
|
type: 'video',
|
||||||
|
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-companion-demo-3.mp4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'showcases.taskPanel.installation.title',
|
||||||
|
descriptionKey: 'showcases.taskPanel.installation.description',
|
||||||
|
media: {
|
||||||
|
type: 'video',
|
||||||
|
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-install-companion-3.mp4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'showcases.taskPanel.codeReview.title',
|
||||||
|
descriptionKey: 'showcases.taskPanel.codeReview.description',
|
||||||
|
media: {
|
||||||
|
type: 'video',
|
||||||
|
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-code-review-3.mp4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'showcases.taskPanel.pullRequest.title',
|
||||||
|
descriptionKey: 'showcases.taskPanel.pullRequest.description',
|
||||||
|
media: {
|
||||||
|
type: 'video',
|
||||||
|
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-git-pr-3.mp4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies ShowcaseConfig,
|
||||||
|
} as const;
|
||||||
|
|||||||
42
frontend/src/hooks/useShowcasePersistence.ts
Normal file
42
frontend/src/hooks/useShowcasePersistence.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useUserSystem } from '@/components/config-provider';
|
||||||
|
|
||||||
|
export interface ShowcasePersistence {
|
||||||
|
hasSeen: (id: string) => boolean;
|
||||||
|
markSeen: (id: string) => Promise<void>;
|
||||||
|
isLoaded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShowcasePersistence(): ShowcasePersistence {
|
||||||
|
const { config, updateAndSaveConfig, loading } = useUserSystem();
|
||||||
|
|
||||||
|
const seenFeatures = config?.showcases?.seen_features ?? [];
|
||||||
|
|
||||||
|
const hasSeen = useCallback(
|
||||||
|
(id: string): boolean => {
|
||||||
|
return seenFeatures.includes(id);
|
||||||
|
},
|
||||||
|
[seenFeatures]
|
||||||
|
);
|
||||||
|
|
||||||
|
const markSeen = useCallback(
|
||||||
|
async (id: string): Promise<void> => {
|
||||||
|
if (seenFeatures.includes(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateAndSaveConfig({
|
||||||
|
showcases: {
|
||||||
|
seen_features: [...seenFeatures, id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[seenFeatures, updateAndSaveConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasSeen,
|
||||||
|
markSeen,
|
||||||
|
isLoaded: !loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import type { ShowcaseConfig } from '@/types/showcase';
|
import type { ShowcaseConfig } from '@/types/showcase';
|
||||||
import { hasSeen as hasSeenUtil, markSeen } from '@/utils/showcasePersistence';
|
import { useShowcasePersistence } from './useShowcasePersistence';
|
||||||
|
|
||||||
export interface ShowcaseTriggerOptions {
|
export interface ShowcaseTriggerOptions {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -27,17 +27,17 @@ export function useShowcaseTrigger(
|
|||||||
markSeenOnClose = true,
|
markSeenOnClose = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
const persistence = useShowcasePersistence();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [hasSeen, setHasSeen] = useState(() =>
|
const [hasSeenState, setHasSeenState] = useState(false);
|
||||||
hasSeenUtil(config.id, config.version)
|
|
||||||
);
|
|
||||||
const timerRef = useRef<number | null>(null);
|
const timerRef = useRef<number | null>(null);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
|
|
||||||
// Keep 'hasSeen' in sync if id/version change
|
// Keep 'hasSeenState' in sync if id change or config loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasSeen(hasSeenUtil(config.id, config.version));
|
if (!persistence.isLoaded) return;
|
||||||
}, [config.id, config.version]);
|
setHasSeenState(persistence.hasSeen(config.id));
|
||||||
|
}, [persistence.isLoaded, config.id, persistence]);
|
||||||
|
|
||||||
// Cleanup timers
|
// Cleanup timers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,9 +53,11 @@ export function useShowcaseTrigger(
|
|||||||
|
|
||||||
// Handle enabled state changes
|
// Handle enabled state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!persistence.isLoaded) return;
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
// Only show if not seen
|
// Only show if not seen
|
||||||
if (!hasSeen) {
|
if (!hasSeenState) {
|
||||||
// Clear any existing timer
|
// Clear any existing timer
|
||||||
if (timerRef.current !== null) {
|
if (timerRef.current !== null) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
@@ -87,7 +89,7 @@ export function useShowcaseTrigger(
|
|||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [enabled, hasSeen, openDelay, resetOnDisable]);
|
}, [persistence.isLoaded, enabled, hasSeenState, openDelay, resetOnDisable]);
|
||||||
|
|
||||||
const open = useCallback(() => {
|
const open = useCallback(() => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
@@ -95,15 +97,15 @@ export function useShowcaseTrigger(
|
|||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => {
|
||||||
if (markSeenOnClose) {
|
if (markSeenOnClose) {
|
||||||
markSeen(config.id, config.version);
|
persistence.markSeen(config.id);
|
||||||
setHasSeen(true);
|
setHasSeenState(true);
|
||||||
}
|
}
|
||||||
if (timerRef.current !== null) {
|
if (timerRef.current !== null) {
|
||||||
clearTimeout(timerRef.current);
|
clearTimeout(timerRef.current);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
}
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}, [config.id, config.version, markSeenOnClose]);
|
}, [config.id, markSeenOnClose, persistence]);
|
||||||
|
|
||||||
return { isOpen, open, close, hasSeen };
|
return { isOpen, open, close, hasSeen: hasSeenState };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { tasksApi } from '@/lib/api';
|
|||||||
import type { GitBranch } from 'shared/types';
|
import type { GitBranch } from 'shared/types';
|
||||||
import { openTaskForm } from '@/lib/openTaskForm';
|
import { openTaskForm } from '@/lib/openTaskForm';
|
||||||
import { FeatureShowcaseModal } from '@/components/showcase/FeatureShowcaseModal';
|
import { FeatureShowcaseModal } from '@/components/showcase/FeatureShowcaseModal';
|
||||||
import { taskPanelShowcase } from '@/config/showcases';
|
import { showcases } from '@/config/showcases';
|
||||||
import { useShowcaseTrigger } from '@/hooks/useShowcaseTrigger';
|
import { useShowcaseTrigger } from '@/hooks/useShowcaseTrigger';
|
||||||
|
|
||||||
import { useSearch } from '@/contexts/search-context';
|
import { useSearch } from '@/contexts/search-context';
|
||||||
@@ -157,7 +157,7 @@ export function ProjectTasks() {
|
|||||||
const isPanelOpen = Boolean(taskId && selectedTask);
|
const isPanelOpen = Boolean(taskId && selectedTask);
|
||||||
|
|
||||||
const { isOpen: showTaskPanelShowcase, close: closeTaskPanelShowcase } =
|
const { isOpen: showTaskPanelShowcase, close: closeTaskPanelShowcase } =
|
||||||
useShowcaseTrigger(taskPanelShowcase, {
|
useShowcaseTrigger(showcases.taskPanel, {
|
||||||
enabled: isPanelOpen,
|
enabled: isPanelOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -766,7 +766,7 @@ export function ProjectTasks() {
|
|||||||
<FeatureShowcaseModal
|
<FeatureShowcaseModal
|
||||||
isOpen={showTaskPanelShowcase}
|
isOpen={showTaskPanelShowcase}
|
||||||
onClose={closeTaskPanelShowcase}
|
onClose={closeTaskPanelShowcase}
|
||||||
config={taskPanelShowcase}
|
config={showcases.taskPanel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,5 @@ export interface ShowcaseStage {
|
|||||||
|
|
||||||
export interface ShowcaseConfig {
|
export interface ShowcaseConfig {
|
||||||
id: string;
|
id: string;
|
||||||
version: number;
|
|
||||||
stages: ShowcaseStage[];
|
stages: ShowcaseStage[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* Check if a user has seen a specific showcase version
|
|
||||||
*
|
|
||||||
* @param id - Unique identifier for the showcase
|
|
||||||
* @param version - Version number for the showcase
|
|
||||||
* @returns true if the user has seen this showcase version
|
|
||||||
*
|
|
||||||
* Storage key format: `showcase:{id}:v{version}:seen`
|
|
||||||
*/
|
|
||||||
export function hasSeen(id: string, version: number): boolean {
|
|
||||||
const key = `showcase:${id}:v${version}:seen`;
|
|
||||||
return localStorage.getItem(key) === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a showcase as seen
|
|
||||||
*
|
|
||||||
* @param id - Unique identifier for the showcase
|
|
||||||
* @param version - Version number for the showcase
|
|
||||||
*
|
|
||||||
* Storage key format: `showcase:{id}:v{version}:seen`
|
|
||||||
*/
|
|
||||||
export function markSeen(id: string, version: number): void {
|
|
||||||
const key = `showcase:${id}:v${version}:seen`;
|
|
||||||
localStorage.setItem(key, 'true');
|
|
||||||
}
|
|
||||||
@@ -92,7 +92,7 @@ export type ImageResponse = { id: string, file_path: string, original_name: stri
|
|||||||
|
|
||||||
export enum GitHubServiceError { TOKEN_INVALID = "TOKEN_INVALID", INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS", REPO_NOT_FOUND_OR_NO_ACCESS = "REPO_NOT_FOUND_OR_NO_ACCESS" }
|
export enum GitHubServiceError { TOKEN_INVALID = "TOKEN_INVALID", INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS", REPO_NOT_FOUND_OR_NO_ACCESS = "REPO_NOT_FOUND_OR_NO_ACCESS" }
|
||||||
|
|
||||||
export type Config = { config_version: string, theme: ThemeMode, executor_profile: ExecutorProfileId, disclaimer_acknowledged: boolean, onboarding_acknowledged: boolean, github_login_acknowledged: boolean, telemetry_acknowledged: boolean, notifications: NotificationConfig, editor: EditorConfig, github: GitHubConfig, analytics_enabled: boolean | null, workspace_dir: string | null, last_app_version: string | null, show_release_notes: boolean, language: UiLanguage, git_branch_prefix: string, };
|
export type Config = { config_version: string, theme: ThemeMode, executor_profile: ExecutorProfileId, disclaimer_acknowledged: boolean, onboarding_acknowledged: boolean, github_login_acknowledged: boolean, telemetry_acknowledged: boolean, notifications: NotificationConfig, editor: EditorConfig, github: GitHubConfig, analytics_enabled: boolean | null, workspace_dir: string | null, last_app_version: string | null, show_release_notes: boolean, language: UiLanguage, git_branch_prefix: string, showcases: ShowcaseState, };
|
||||||
|
|
||||||
export type NotificationConfig = { sound_enabled: boolean, push_enabled: boolean, sound_file: SoundFile, };
|
export type NotificationConfig = { sound_enabled: boolean, push_enabled: boolean, sound_file: SoundFile, };
|
||||||
|
|
||||||
@@ -108,6 +108,8 @@ export enum SoundFile { ABSTRACT_SOUND1 = "ABSTRACT_SOUND1", ABSTRACT_SOUND2 = "
|
|||||||
|
|
||||||
export type UiLanguage = "BROWSER" | "EN" | "JA" | "ES" | "KO";
|
export type UiLanguage = "BROWSER" | "EN" | "JA" | "ES" | "KO";
|
||||||
|
|
||||||
|
export type ShowcaseState = { seen_features: Array<string>, };
|
||||||
|
|
||||||
export type DeviceFlowStartResponse = { user_code: string, verification_uri: string, expires_in: number, interval: number, };
|
export type DeviceFlowStartResponse = { user_code: string, verification_uri: string, expires_in: number, interval: number, };
|
||||||
|
|
||||||
export enum DevicePollStatus { SLOW_DOWN = "SLOW_DOWN", AUTHORIZATION_PENDING = "AUTHORIZATION_PENDING", SUCCESS = "SUCCESS" }
|
export enum DevicePollStatus { SLOW_DOWN = "SLOW_DOWN", AUTHORIZATION_PENDING = "AUTHORIZATION_PENDING", SUCCESS = "SUCCESS" }
|
||||||
|
|||||||
Reference in New Issue
Block a user