@umituz/react-native-ai-generation-content 1.72.5 → 1.72.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/package.json +1 -1
  2. package/src/domains/background/infrastructure/services/job-poller.service.ts +20 -0
  3. package/src/domains/background/presentation/hooks/use-pending-jobs.ts +6 -3
  4. package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +7 -1
  5. package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +39 -6
  6. package/src/domains/generation/infrastructure/flow/useFlowStore.ts +19 -9
  7. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +8 -5
  8. package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts +20 -2
  9. package/src/domains/generation/wizard/presentation/hooks/generationExecutor.ts +6 -2
  10. package/src/domains/generation/wizard/presentation/hooks/usePhotoUploadState.ts +21 -7
  11. package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +22 -4
  12. package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +9 -1
  13. package/src/domains/generation/wizard/presentation/hooks/videoQueuePoller.ts +6 -3
  14. package/src/domains/image-to-video/presentation/hooks/imageToVideoStrategy.ts +7 -1
  15. package/src/domains/result-preview/presentation/hooks/useResultActions.ts +6 -1
  16. package/src/domains/text-to-video/presentation/hooks/textToVideoStrategy.ts +7 -1
  17. package/src/infrastructure/services/generation-orchestrator.service.ts +5 -0
  18. package/src/infrastructure/services/image-feature-executor.service.ts +6 -1
  19. package/src/infrastructure/validation/base-validator.ts +4 -0
  20. package/src/presentation/hooks/flow-state.utils.ts +6 -0
  21. package/src/presentation/hooks/generation/orchestrator.ts +19 -5
  22. package/src/presentation/hooks/generation-flow-navigation.ts +13 -2
  23. package/src/presentation/hooks/use-generation.ts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.72.5",
3
+ "version": "1.72.7",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -77,6 +77,26 @@ export async function pollJob<T = unknown>(
77
77
  signal,
78
78
  } = options;
79
79
 
80
+ // Validate requestId early
81
+ if (!requestId || typeof requestId !== "string" || requestId.trim() === "") {
82
+ return {
83
+ success: false,
84
+ error: new Error("Invalid requestId provided"),
85
+ attempts: 0,
86
+ elapsedMs: 0,
87
+ };
88
+ }
89
+
90
+ // Validate model early
91
+ if (!model || typeof model !== "string" || model.trim() === "") {
92
+ return {
93
+ success: false,
94
+ error: new Error("Invalid model provided"),
95
+ attempts: 0,
96
+ elapsedMs: 0,
97
+ };
98
+ }
99
+
80
100
  const pollingConfig = { ...DEFAULT_POLLING_CONFIG, ...config };
81
101
  const { maxAttempts, maxTotalTimeMs, maxConsecutiveErrors } = pollingConfig;
82
102
 
@@ -85,7 +85,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
85
85
  onSuccess: (id: string) => {
86
86
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
87
87
  queryKey,
88
- (old: BackgroundJob<TInput, TResult>[] | undefined) => old?.filter((job: BackgroundJob<TInput, TResult>) => job.id !== id) ?? [],
88
+ (old: BackgroundJob<TInput, TResult>[] | undefined) =>
89
+ old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.id !== id) ?? [],
89
90
  );
90
91
  },
91
92
  });
@@ -95,7 +96,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
95
96
  onSuccess: () => {
96
97
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
97
98
  queryKey,
98
- (old: BackgroundJob<TInput, TResult>[] | undefined) => old?.filter((job: BackgroundJob<TInput, TResult>) => job.status !== "completed") ?? [],
99
+ (old: BackgroundJob<TInput, TResult>[] | undefined) =>
100
+ old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.status !== "completed") ?? [],
99
101
  );
100
102
  },
101
103
  });
@@ -105,7 +107,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
105
107
  onSuccess: () => {
106
108
  queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
107
109
  queryKey,
108
- (old: BackgroundJob<TInput, TResult>[] | undefined) => old?.filter((job: BackgroundJob<TInput, TResult>) => job.status !== "failed") ?? [],
110
+ (old: BackgroundJob<TInput, TResult>[] | undefined) =>
111
+ old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.status !== "failed") ?? [],
109
112
  );
110
113
  },
111
114
  });
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import { useCallback, useMemo } from "react";
6
+
7
+ declare const __DEV__: boolean;
6
8
  import { useAuth } from "@umituz/react-native-auth";
7
9
  import { createCreationsRepository } from "../../infrastructure/adapters";
8
10
  import type { Creation } from "../../domain/entities/Creation";
@@ -68,7 +70,11 @@ export function useCreationPersistence(
68
70
  });
69
71
 
70
72
  if (creditCost && onCreditDeduct) {
71
- onCreditDeduct(creditCost).catch(() => {});
73
+ onCreditDeduct(creditCost).catch((error) => {
74
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
75
+ console.error("[CreationPersistence] Credit deduction failed:", error);
76
+ }
77
+ });
72
78
  }
73
79
  },
74
80
  [userId, repository, creditCost, onCreditDeduct]
@@ -42,6 +42,14 @@ export function useProcessingJobsPoller(
42
42
  const pollingRef = useRef<Set<string>>(new Set());
43
43
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
44
44
 
45
+ // Convert to IDs to prevent re-creating array on every render
46
+ const processingJobIds = useMemo(
47
+ () => creations
48
+ .filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
49
+ .map((c) => c.id),
50
+ [creations],
51
+ );
52
+
45
53
  const processingJobs = useMemo(
46
54
  () => creations.filter(
47
55
  (c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
@@ -81,6 +89,19 @@ export function useProcessingJobsPoller(
81
89
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
82
90
 
83
91
  const uri = urls.videoUrl || urls.imageUrl || "";
92
+
93
+ // Validate that we have a valid URI before marking as completed
94
+ if (!uri || uri.trim() === "") {
95
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
96
+ console.error("[ProcessingJobsPoller] No valid URI in result:", creation.id);
97
+ }
98
+ await repository.update(userId, creation.id, {
99
+ status: CREATION_STATUS.FAILED,
100
+ metadata: { error: "No valid result URL received" },
101
+ });
102
+ return;
103
+ }
104
+
84
105
  await repository.update(userId, creation.id, {
85
106
  status: CREATION_STATUS.COMPLETED,
86
107
  uri,
@@ -103,8 +124,14 @@ export function useProcessingJobsPoller(
103
124
  }
104
125
  };
105
126
 
127
+ // Use ref to always get latest creations
128
+ const creationsRef = useRef(creations);
106
129
  useEffect(() => {
107
- if (!enabled || !userId || processingJobs.length === 0) {
130
+ creationsRef.current = creations;
131
+ }, [creations]);
132
+
133
+ useEffect(() => {
134
+ if (!enabled || !userId || processingJobIds.length === 0) {
108
135
  if (intervalRef.current) {
109
136
  clearInterval(intervalRef.current);
110
137
  intervalRef.current = null;
@@ -112,13 +139,19 @@ export function useProcessingJobsPoller(
112
139
  return;
113
140
  }
114
141
 
142
+ // Get current jobs at poll time from ref to avoid stale closures
143
+ const pollCurrentJobs = () => {
144
+ const currentJobs = creationsRef.current.filter(
145
+ (c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
146
+ );
147
+ currentJobs.forEach((job) => pollJobRef.current?.(job));
148
+ };
149
+
115
150
  // Initial poll
116
- processingJobs.forEach((job) => pollJobRef.current?.(job));
151
+ pollCurrentJobs();
117
152
 
118
153
  // Set up interval polling
119
- intervalRef.current = setInterval(() => {
120
- processingJobs.forEach((job) => pollJobRef.current?.(job));
121
- }, DEFAULT_POLL_INTERVAL_MS);
154
+ intervalRef.current = setInterval(pollCurrentJobs, DEFAULT_POLL_INTERVAL_MS);
122
155
 
123
156
  return () => {
124
157
  // Clear polling set first to prevent new operations
@@ -130,7 +163,7 @@ export function useProcessingJobsPoller(
130
163
  intervalRef.current = null;
131
164
  }
132
165
  };
133
- }, [enabled, userId, processingJobs]);
166
+ }, [enabled, userId, processingJobIds]);
134
167
 
135
168
  return {
136
169
  processingCount: processingJobs.length,
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import { createStore } from "@umituz/react-native-design-system";
6
+
7
+ declare const __DEV__: boolean;
6
8
  import type {
7
9
  FlowState,
8
10
  FlowActions,
@@ -34,7 +36,13 @@ export const createFlowStore = (config: FlowStoreConfig) => {
34
36
  if (config.initialStepIndex !== undefined) {
35
37
  initialIndex = Math.max(0, Math.min(config.initialStepIndex, config.steps.length - 1));
36
38
  } else if (config.initialStepId) {
37
- initialIndex = Math.max(0, config.steps.findIndex((s) => s.id === config.initialStepId));
39
+ const foundIndex = config.steps.findIndex((s) => s.id === config.initialStepId);
40
+ if (foundIndex === -1) {
41
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
42
+ console.warn(`[FlowStore] Step "${config.initialStepId}" not found, using first step`);
43
+ }
44
+ }
45
+ initialIndex = Math.max(0, foundIndex);
38
46
  }
39
47
 
40
48
  const initialStepId = config.steps[initialIndex]?.id ?? "";
@@ -86,28 +94,30 @@ export const createFlowStore = (config: FlowStoreConfig) => {
86
94
  setScenario: (scenario: unknown) => set({ selectedScenario: scenario }),
87
95
 
88
96
  setPartnerImage: (partnerId: string, image: FlowUploadedImageData | undefined) =>
89
- set({ partners: { ...get().partners, [partnerId]: image } }),
97
+ set((state) => ({ partners: { ...state.partners, [partnerId]: image } })),
90
98
 
91
99
  setPartnerName: (partnerId: string, name: string) =>
92
- set({ partnerNames: { ...get().partnerNames, [partnerId]: name } }),
100
+ set((state) => ({ partnerNames: { ...state.partnerNames, [partnerId]: name } })),
93
101
 
94
102
  setTextInput: (text: string) => set({ textInput: text }),
95
103
  setVisualStyle: (styleId: string) => set({ visualStyle: styleId }),
96
104
 
97
105
  setSelectedFeatures: (featureType: string, ids: readonly string[]) =>
98
- set({ selectedFeatures: { ...get().selectedFeatures, [featureType]: ids } }),
106
+ set((state) => ({ selectedFeatures: { ...state.selectedFeatures, [featureType]: ids } })),
99
107
 
100
108
  setCustomData: (key: string, value: unknown) =>
101
- set({ customData: { ...get().customData, [key]: value } }),
109
+ set((state) => ({ customData: { ...state.customData, [key]: value } })),
102
110
 
103
111
  startGeneration: () =>
104
112
  set({ generationStatus: "preparing", generationProgress: 0, generationError: undefined }),
105
113
 
106
- updateProgress: (progress: number) =>
114
+ updateProgress: (progress: number) => {
115
+ const validProgress = Math.max(0, Math.min(100, progress));
107
116
  set({
108
- generationProgress: progress,
109
- generationStatus: progress >= 100 ? "completed" : progress > 0 ? "generating" : "preparing",
110
- }),
117
+ generationProgress: validProgress,
118
+ generationStatus: validProgress >= 100 ? "completed" : validProgress > 0 ? "generating" : "preparing",
119
+ });
120
+ },
111
121
 
112
122
  setResult: (result: unknown) =>
113
123
  set({ generationResult: result, generationStatus: "completed", generationProgress: 100 }),
@@ -83,14 +83,17 @@ function applyStyleEnhancements(prompt: string, wizardData: Record<string, unkno
83
83
  export function createImageStrategy(options: CreateImageStrategyOptions): WizardStrategy {
84
84
  const { scenario, creditCost } = options;
85
85
 
86
+ // Validate model early - fail fast
87
+ if (!scenario.model) {
88
+ throw new Error("Model is required for image generation");
89
+ }
90
+
91
+ const model = scenario.model;
92
+
86
93
  return {
87
94
  execute: async (input: unknown) => {
88
95
  const imageInput = input as WizardImageInput;
89
- if (!scenario.model) {
90
- throw new Error("Model is required for image generation");
91
- }
92
-
93
- const result = await executeImageGeneration(imageInput, scenario.model);
96
+ const result = await executeImageGeneration(imageInput, model);
94
97
 
95
98
  if (!result.success || !result.imageUrl) {
96
99
  throw new Error(result.error || "Image generation failed");
@@ -55,13 +55,31 @@ export async function extractPhotosAsBase64(
55
55
  return [];
56
56
  }
57
57
 
58
- const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
59
- const validPhotos = photosBase64.filter(Boolean) as string[];
58
+ // Use Promise.allSettled to handle individual failures gracefully
59
+ const results = await Promise.allSettled(
60
+ photoUris.map(async (uri, index) => {
61
+ try {
62
+ return await readFileAsBase64(uri);
63
+ } catch (error) {
64
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
65
+ console.error(`[PhotoExtraction] Failed to read photo ${index}:`, error);
66
+ }
67
+ return null;
68
+ }
69
+ })
70
+ );
71
+
72
+ // Extract successful results only
73
+ const validPhotos = results
74
+ .map((result) => (result.status === "fulfilled" ? result.value : null))
75
+ .filter((photo): photo is string => typeof photo === "string" && photo.length > 0);
60
76
 
61
77
  if (enableDebugLogs && typeof __DEV__ !== "undefined" && __DEV__) {
78
+ const failedCount = results.filter((r) => r.status === "rejected").length;
62
79
  console.log("[PhotoExtraction] Converted photos", {
63
80
  total: photoUris.length,
64
81
  valid: validPhotos.length,
82
+ failed: failedCount,
65
83
  sizes: validPhotos.map((p) => `${(p.length / 1024).toFixed(1)}KB`),
66
84
  });
67
85
  }
@@ -44,9 +44,13 @@ export const executeWizardGeneration = async (params: ExecuteGenerationParams):
44
44
  console.log("[WizardGeneration] GENERATING -", isVideoMode ? "VIDEO" : "PHOTO");
45
45
  }
46
46
 
47
- const typedInput = input as { prompt?: string };
47
+ // Safely extract prompt with type guard
48
+ const prompt = typeof input === "object" && input !== null && "prompt" in input && typeof input.prompt === "string"
49
+ ? input.prompt
50
+ : "";
51
+
48
52
  const generationFn = isVideoMode ? videoGenerationFn : photoGenerationFn;
49
- await generationFn(input, typedInput.prompt || "");
53
+ await generationFn(input, prompt);
50
54
 
51
55
  if (isMountedRef.current) {
52
56
  dispatch({ type: "COMPLETE" });
@@ -5,6 +5,8 @@
5
5
  */
6
6
 
7
7
  import { useState, useCallback, useEffect, useRef } from "react";
8
+
9
+ declare const __DEV__: boolean;
8
10
  import { useMedia, MediaQuality, MediaValidationError, MEDIA_CONSTANTS } from "@umituz/react-native-design-system";
9
11
  import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
10
12
 
@@ -51,6 +53,15 @@ export const usePhotoUploadState = ({
51
53
  const { pickImage, isLoading } = useMedia();
52
54
  const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
53
55
 
56
+ // Use refs to avoid effect re-runs on callback changes
57
+ const onErrorRef = useRef(onError);
58
+ const translationsRef = useRef(translations);
59
+
60
+ useEffect(() => {
61
+ onErrorRef.current = onError;
62
+ translationsRef.current = translations;
63
+ }, [onError, translations]);
64
+
54
65
  const maxFileSizeMB = config?.maxFileSizeMB ?? MEDIA_CONSTANTS.MAX_IMAGE_SIZE_MB;
55
66
 
56
67
  useEffect(() => {
@@ -61,28 +72,31 @@ export const usePhotoUploadState = ({
61
72
  }, [stepId, initialImage]);
62
73
 
63
74
  useEffect(() => {
75
+ // Clear any existing timeout first
76
+ if (timeoutRef.current) {
77
+ clearTimeout(timeoutRef.current);
78
+ timeoutRef.current = undefined;
79
+ }
80
+
64
81
  if (isLoading) {
65
82
  timeoutRef.current = setTimeout(() => {
66
83
  if (typeof __DEV__ !== "undefined" && __DEV__) {
67
84
  console.warn("[usePhotoUploadState] Image picker timeout - possible stuck state");
68
85
  }
69
- onError?.({
70
- title: translations.error,
86
+ onErrorRef.current?.({
87
+ title: translationsRef.current.error,
71
88
  message: "Image selection is taking too long. Please try again.",
72
89
  });
73
90
  }, 30000);
74
- } else {
75
- if (timeoutRef.current) {
76
- clearTimeout(timeoutRef.current);
77
- }
78
91
  }
79
92
 
80
93
  return () => {
81
94
  if (timeoutRef.current) {
82
95
  clearTimeout(timeoutRef.current);
96
+ timeoutRef.current = undefined;
83
97
  }
84
98
  };
85
- }, [isLoading, onError, translations]);
99
+ }, [isLoading]);
86
100
 
87
101
  const clearImage = useCallback(() => {
88
102
  setImage(null);
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import { useEffect, useRef, useCallback, useState } from "react";
6
+
7
+ declare const __DEV__: boolean;
6
8
  import { pollQueueStatus } from "./videoQueuePoller";
7
9
  import { DEFAULT_POLL_INTERVAL_MS } from "../../../../../infrastructure/constants/polling.constants";
8
10
  import type { GenerationUrls } from "./generation-result.utils";
@@ -50,7 +52,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
50
52
  imageUrl: urls.imageUrl,
51
53
  videoUrl: urls.videoUrl,
52
54
  });
53
- } catch {}
55
+ } catch (error) {
56
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
57
+ console.error("[VideoQueue] Failed to update completion status:", error);
58
+ }
59
+ }
54
60
  }
55
61
  resetRefs();
56
62
  onSuccess?.(urls);
@@ -64,7 +70,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
64
70
  if (creationId && userId) {
65
71
  try {
66
72
  await persistence.updateToFailed(userId, creationId, errorMsg);
67
- } catch {}
73
+ } catch (error) {
74
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
75
+ console.error("[VideoQueue] Failed to update error status:", error);
76
+ }
77
+ }
68
78
  }
69
79
  resetRefs();
70
80
  onError?.(errorMsg);
@@ -107,7 +117,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
107
117
  prompt,
108
118
  });
109
119
  creationIdRef.current = creationId;
110
- } catch {}
120
+ } catch (error) {
121
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
122
+ console.error("[VideoQueue] Failed to save processing creation:", error);
123
+ }
124
+ }
111
125
  }
112
126
 
113
127
  const queueResult = await strategy.submitToQueue(input);
@@ -126,7 +140,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
126
140
  if (creationId && userId && queueResult.requestId && queueResult.model) {
127
141
  try {
128
142
  await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
129
- } catch {}
143
+ } catch (error) {
144
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
145
+ console.error("[VideoQueue] Failed to update request ID:", error);
146
+ }
147
+ }
130
148
  }
131
149
 
132
150
  pollingRef.current = setInterval(() => void pollStatus(), DEFAULT_POLL_INTERVAL_MS);
@@ -72,7 +72,8 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
72
72
  if (isGeneratingStep && state.status === "IDLE" && !isAlreadyGenerating) {
73
73
  dispatch({ type: "START_PREPARATION" });
74
74
 
75
- executeWizardGeneration({
75
+ // Execute generation and handle errors properly
76
+ void executeWizardGeneration({
76
77
  wizardData,
77
78
  scenario,
78
79
  isVideoMode,
@@ -81,6 +82,13 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
81
82
  onError,
82
83
  videoGenerationFn: videoGeneration.startGeneration,
83
84
  photoGenerationFn: photoGeneration.startGeneration,
85
+ }).catch((error) => {
86
+ // Catch any unhandled errors from executeWizardGeneration
87
+ if (isMountedRef.current) {
88
+ const errorMsg = error instanceof Error ? error.message : String(error);
89
+ dispatch({ type: "ERROR", error: errorMsg });
90
+ onError?.(errorMsg);
91
+ }
84
92
  });
85
93
  }
86
94
 
@@ -16,12 +16,15 @@ interface PollParams {
16
16
  export const pollQueueStatus = async (params: PollParams): Promise<void> => {
17
17
  const { requestId, model, isPollingRef, pollingRef, onComplete, onError } = params;
18
18
 
19
+ // Atomic check-and-set to prevent race condition
19
20
  if (isPollingRef.current) return;
21
+ isPollingRef.current = true;
20
22
 
21
23
  const provider = providerRegistry.getActiveProvider();
22
- if (!provider) return;
23
-
24
- isPollingRef.current = true;
24
+ if (!provider) {
25
+ isPollingRef.current = false;
26
+ return;
27
+ }
25
28
  try {
26
29
  const status = await provider.getJobStatus(model, requestId);
27
30
  if (__DEV__) console.log("[VideoQueueGeneration] Poll:", status.status);
@@ -1,4 +1,6 @@
1
1
  import { executeImageToVideo } from "../../infrastructure/services";
2
+
3
+ declare const __DEV__: boolean;
2
4
  import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
3
5
  import type {
4
6
  ImageToVideoFeatureConfig,
@@ -41,7 +43,11 @@ export const createImageToVideoStrategy = (
41
43
  type: "image-to-video",
42
44
  imageUri: input.imageUrl,
43
45
  metadata: input.options as Record<string, unknown> | undefined,
44
- }).catch(() => {});
46
+ }).catch((error) => {
47
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
48
+ console.error("[ImageToVideo] onGenerationStart callback failed:", error);
49
+ }
50
+ });
45
51
 
46
52
  const result = await executeImageToVideo(
47
53
  { imageUri: input.imageUrl, userId, motionPrompt: input.prompt, options: input.options },
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import { useState, useCallback } from "react";
7
+
8
+ declare const __DEV__: boolean;
7
9
  import * as MediaLibrary from "expo-media-library";
8
10
  import * as Sharing from "expo-sharing";
9
11
  import {
@@ -88,7 +90,10 @@ export const useResultActions = (options: UseResultActionsOptions = {}): UseResu
88
90
  onShareEnd?.(true);
89
91
  }
90
92
  } catch (error: unknown) {
91
- if (__DEV__) console.log("Share cancelled or failed:", error);
93
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
94
+ const errorMsg = error instanceof Error ? error.message : String(error);
95
+ console.log("[ResultActions] Share cancelled or failed:", errorMsg);
96
+ }
92
97
  onShareEnd?.(true);
93
98
  } finally {
94
99
  setIsSharing(false);
@@ -1,4 +1,6 @@
1
1
  import { executeTextToVideo } from "../../infrastructure/services";
2
+
3
+ declare const __DEV__: boolean;
2
4
  import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
3
5
  import type {
4
6
  TextToVideoConfig,
@@ -40,7 +42,11 @@ export const createTextToVideoStrategy = (
40
42
  type: "text-to-video",
41
43
  prompt: input.prompt,
42
44
  metadata: input.options as Record<string, unknown> | undefined,
43
- }).catch(() => {});
45
+ }).catch((error) => {
46
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
47
+ console.error("[TextToVideo] onGenerationStart callback failed:", error);
48
+ }
49
+ });
44
50
 
45
51
  const result = await executeTextToVideo(
46
52
  { prompt: input.prompt, userId, options: input.options },
@@ -86,6 +86,11 @@ class GenerationOrchestratorService {
86
86
  throw pollResult.error ?? new Error("Polling failed");
87
87
  }
88
88
 
89
+ // Validate result exists before type assertion
90
+ if (!pollResult.data) {
91
+ throw new Error("Polling succeeded but no data returned");
92
+ }
93
+
89
94
  const result = pollResult.data as T;
90
95
  const duration = Date.now() - startTime;
91
96
 
@@ -72,10 +72,15 @@ export async function executeImageFeature(
72
72
  };
73
73
  }
74
74
 
75
+ // Safely extract requestId with type guard
76
+ const requestId = typeof result === "object" && result !== null && "requestId" in result && typeof result.requestId === "string"
77
+ ? result.requestId
78
+ : undefined;
79
+
75
80
  return {
76
81
  success: true,
77
82
  imageUrl,
78
- requestId: (result as { requestId?: string })?.requestId,
83
+ requestId,
79
84
  };
80
85
  } catch (error) {
81
86
  const message = extractErrorMessage(error, "Processing failed", `Image:${featureType}`);
@@ -136,6 +136,10 @@ export function validateBase64(input: unknown): ValidationResult {
136
136
  return { isValid: false, errors: ["Input must be a string"] };
137
137
  }
138
138
 
139
+ if (input.length === 0) {
140
+ return { isValid: false, errors: ["Base64 string cannot be empty"] };
141
+ }
142
+
139
143
  const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
140
144
  if (!base64Regex.test(input)) {
141
145
  return { isValid: false, errors: ["Invalid base64 format"] };
@@ -48,6 +48,12 @@ export function updatePhotoStep(
48
48
  stepIndex: number,
49
49
  updates: Partial<PhotoStepData>,
50
50
  ): GenerationFlowState {
51
+ // Validate bounds to prevent sparse arrays and undefined access
52
+ if (stepIndex < 0 || stepIndex >= state.photoSteps.length) {
53
+ console.warn(`[FlowState] Invalid stepIndex ${stepIndex}, ignoring update`);
54
+ return state;
55
+ }
56
+
51
57
  const newPhotoSteps = [...state.photoSteps];
52
58
  newPhotoSteps[stepIndex] = {
53
59
  ...newPhotoSteps[stepIndex],
@@ -34,6 +34,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
34
34
  const isGeneratingRef = useRef(false);
35
35
  const isMountedRef = useRef(true);
36
36
  const abortControllerRef = useRef<AbortController | null>(null);
37
+ const cleanupTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
37
38
 
38
39
  const offlineStore = useOfflineStore();
39
40
  const { showError, showSuccess } = useAlert();
@@ -44,18 +45,31 @@ export const useGenerationOrchestrator = <TInput, TResult>(
44
45
  return () => {
45
46
  isMountedRef.current = false;
46
47
  abortControllerRef.current?.abort();
48
+ // Clear any pending lifecycle complete timeout
49
+ if (cleanupTimeoutRef.current) {
50
+ clearTimeout(cleanupTimeoutRef.current);
51
+ cleanupTimeoutRef.current = null;
52
+ }
47
53
  };
48
54
  }, []);
49
55
 
50
56
  const handleLifecycleComplete = useCallback(
51
57
  (status: "success" | "error", result?: TResult, error?: GenerationError) => {
52
58
  if (!lifecycle?.onComplete) return;
59
+
60
+ // Clear any existing timeout first
61
+ if (cleanupTimeoutRef.current) {
62
+ clearTimeout(cleanupTimeoutRef.current);
63
+ cleanupTimeoutRef.current = null;
64
+ }
65
+
53
66
  const delay = lifecycle.completeDelay ?? 500;
54
- const timeoutId = setTimeout(() => {
55
- if (isMountedRef.current) lifecycle.onComplete?.(status, result, error);
67
+ cleanupTimeoutRef.current = setTimeout(() => {
68
+ if (isMountedRef.current) {
69
+ lifecycle.onComplete?.(status, result, error);
70
+ }
71
+ cleanupTimeoutRef.current = null;
56
72
  }, delay);
57
- // Store timeout ID for cleanup (if component unmounts during delay)
58
- return () => clearTimeout(timeoutId);
59
73
  },
60
74
  [lifecycle],
61
75
  );
@@ -237,7 +251,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
237
251
  abortControllerRef.current = null;
238
252
  }
239
253
  },
240
- [offlineStore.isOnline, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
254
+ [offlineStore, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
241
255
  );
242
256
 
243
257
  const reset = useCallback(() => {
@@ -24,7 +24,11 @@ export function createGoNextHandler(
24
24
  onComplete?.(newState);
25
25
  } else {
26
26
  setState({ ...state, currentStepIndex: nextIndex });
27
- onStepChange?.(nextIndex, config.photoSteps[nextIndex]);
27
+ // Bounds check before accessing array
28
+ const nextStep = config.photoSteps[nextIndex];
29
+ if (nextStep) {
30
+ onStepChange?.(nextIndex, nextStep);
31
+ }
28
32
  }
29
33
  };
30
34
  }
@@ -40,8 +44,15 @@ export function createGoBackHandler(
40
44
  if (!canGoBack) return;
41
45
 
42
46
  const prevIndex = currentStepIndex - 1;
47
+ // Validate index is within bounds
48
+ if (prevIndex < 0) return;
49
+
43
50
  setState((prev) => ({ ...prev, currentStepIndex: prevIndex, isComplete: false }));
44
- onStepChange?.(prevIndex, config.photoSteps[prevIndex]);
51
+ // Bounds check before accessing array
52
+ const prevStep = config.photoSteps[prevIndex];
53
+ if (prevStep) {
54
+ onStepChange?.(prevIndex, prevStep);
55
+ }
45
56
  };
46
57
  }
47
58
 
@@ -88,7 +88,7 @@ export function useGeneration<T = unknown>(
88
88
 
89
89
  const genResult = await generationOrchestrator.generate<T>(request);
90
90
 
91
- if (abortRef.current || abortControllerRef.current.signal.aborted) return;
91
+ if (abortRef.current || abortControllerRef.current?.signal.aborted) return;
92
92
 
93
93
  setResult(genResult);
94
94
 
@@ -109,6 +109,9 @@ export function useGeneration<T = unknown>(
109
109
  if (!abortRef.current && !abortControllerRef.current?.signal.aborted) {
110
110
  setIsGenerating(false);
111
111
  }
112
+ if (abortControllerRef.current && !abortControllerRef.current.signal.aborted) {
113
+ abortControllerRef.current.abort();
114
+ }
112
115
  abortControllerRef.current = null;
113
116
  }
114
117
  },