@umituz/react-native-ai-generation-content 1.72.4 → 1.72.6

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 (22) hide show
  1. package/package.json +1 -1
  2. package/src/domains/access-control/hooks/useAIFeatureGate.ts +49 -4
  3. package/src/domains/background/infrastructure/services/job-poller.service.ts +20 -0
  4. package/src/domains/background/presentation/hooks/use-pending-jobs.ts +6 -3
  5. package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +10 -1
  6. package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +20 -6
  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/scenarios/presentation/screens/HierarchicalScenarioListScreen.tsx +2 -2
  17. package/src/domains/text-to-video/presentation/hooks/textToVideoStrategy.ts +7 -1
  18. package/src/infrastructure/services/generation-orchestrator.service.ts +5 -0
  19. package/src/infrastructure/services/image-feature-executor.service.ts +6 -1
  20. package/src/presentation/hooks/generation/orchestrator.ts +102 -12
  21. package/src/presentation/hooks/generation-flow-navigation.ts +13 -2
  22. package/src/presentation/hooks/use-generation.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.72.4",
3
+ "version": "1.72.6",
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",
@@ -17,16 +17,34 @@ import type {
17
17
  AIFeatureGateReturn,
18
18
  } from "../types/access-control.types";
19
19
 
20
+ declare const __DEV__: boolean;
21
+
20
22
  const handlePromiseResult = (
21
23
  result: void | Promise<void>,
22
24
  onSuccess?: () => void,
23
25
  onError?: (error: Error) => void,
24
26
  ): void => {
27
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
28
+ console.log("[AIFeatureGate] handlePromiseResult - isPromise:", result instanceof Promise);
29
+ }
25
30
  if (result instanceof Promise) {
26
31
  result
27
- .then(() => onSuccess?.())
28
- .catch((err) => onError?.(err instanceof Error ? err : new Error(String(err))));
32
+ .then(() => {
33
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
34
+ console.log("[AIFeatureGate] Promise resolved, calling onSuccess");
35
+ }
36
+ onSuccess?.();
37
+ })
38
+ .catch((err) => {
39
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
40
+ console.log("[AIFeatureGate] Promise rejected:", err);
41
+ }
42
+ onError?.(err instanceof Error ? err : new Error(String(err)));
43
+ });
29
44
  } else {
45
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
46
+ console.log("[AIFeatureGate] Sync result, calling onSuccess");
47
+ }
30
48
  onSuccess?.();
31
49
  }
32
50
  };
@@ -62,18 +80,45 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
62
80
 
63
81
  const requireFeature = useCallback(
64
82
  (action: () => void | Promise<void>): void => {
83
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
84
+ console.log("[AIFeatureGate] requireFeature called:", {
85
+ isOffline,
86
+ isAuthenticated,
87
+ isCreditsLoaded,
88
+ isPremium,
89
+ creditBalance,
90
+ creditCost,
91
+ hasCredits,
92
+ });
93
+ }
94
+
65
95
  if (isOffline) {
96
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
97
+ console.log("[AIFeatureGate] BLOCKED: User is offline");
98
+ }
66
99
  onNetworkError?.();
67
100
  return;
68
101
  }
69
102
 
70
- if (isAuthenticated && !isCreditsLoaded) return;
103
+ if (isAuthenticated && !isCreditsLoaded) {
104
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
105
+ console.log("[AIFeatureGate] BLOCKED: User authenticated but credits not loaded yet");
106
+ }
107
+ return;
108
+ }
109
+
110
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
111
+ console.log("[AIFeatureGate] Calling requireFeatureFromPackage");
112
+ }
71
113
 
72
114
  requireFeatureFromPackage(() => {
115
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
116
+ console.log("[AIFeatureGate] Inside requireFeatureFromPackage callback - executing action");
117
+ }
73
118
  handlePromiseResult(action(), onSuccess, onError);
74
119
  });
75
120
  },
76
- [isOffline, isAuthenticated, isCreditsLoaded, onNetworkError, requireFeatureFromPackage, onSuccess, onError],
121
+ [isOffline, isAuthenticated, isCreditsLoaded, isPremium, creditBalance, creditCost, hasCredits, onNetworkError, requireFeatureFromPackage, onSuccess, onError],
77
122
  );
78
123
 
79
124
  return {
@@ -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,14 @@ export function useCreationPersistence(
68
70
  });
69
71
 
70
72
  if (creditCost && onCreditDeduct) {
71
- onCreditDeduct(creditCost).catch(() => {});
73
+ onCreditDeduct(creditCost).catch((error) => {
74
+ // Log credit deduction errors for debugging
75
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
76
+ console.error("[CreationPersistence] Credit deduction failed:", error);
77
+ }
78
+ // Don't throw - credit deduction failure shouldn't block completion
79
+ // But we should notify user or retry in production
80
+ });
72
81
  }
73
82
  },
74
83
  [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,
@@ -104,7 +112,7 @@ export function useProcessingJobsPoller(
104
112
  };
105
113
 
106
114
  useEffect(() => {
107
- if (!enabled || !userId || processingJobs.length === 0) {
115
+ if (!enabled || !userId || processingJobIds.length === 0) {
108
116
  if (intervalRef.current) {
109
117
  clearInterval(intervalRef.current);
110
118
  intervalRef.current = null;
@@ -112,13 +120,19 @@ export function useProcessingJobsPoller(
112
120
  return;
113
121
  }
114
122
 
123
+ // Get current jobs at poll time to avoid stale closures
124
+ const pollCurrentJobs = () => {
125
+ const currentJobs = creations.filter(
126
+ (c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
127
+ );
128
+ currentJobs.forEach((job) => pollJobRef.current?.(job));
129
+ };
130
+
115
131
  // Initial poll
116
- processingJobs.forEach((job) => pollJobRef.current?.(job));
132
+ pollCurrentJobs();
117
133
 
118
134
  // Set up interval polling
119
- intervalRef.current = setInterval(() => {
120
- processingJobs.forEach((job) => pollJobRef.current?.(job));
121
- }, DEFAULT_POLL_INTERVAL_MS);
135
+ intervalRef.current = setInterval(pollCurrentJobs, DEFAULT_POLL_INTERVAL_MS);
122
136
 
123
137
  return () => {
124
138
  // Clear polling set first to prevent new operations
@@ -130,7 +144,7 @@ export function useProcessingJobsPoller(
130
144
  intervalRef.current = null;
131
145
  }
132
146
  };
133
- }, [enabled, userId, processingJobs]);
147
+ }, [enabled, userId, processingJobIds, creations]);
134
148
 
135
149
  return {
136
150
  processingCount: processingJobs.length,
@@ -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);
@@ -39,7 +39,7 @@ export const HierarchicalScenarioListScreen: React.FC<HierarchicalScenarioListSc
39
39
  onSelectScenario,
40
40
  onBack,
41
41
  t,
42
- numColumns = 1,
42
+ numColumns = 2,
43
43
  isLoading = false,
44
44
  }) => {
45
45
  const tokens = useAppDesignTokens();
@@ -133,7 +133,7 @@ export const HierarchicalScenarioListScreen: React.FC<HierarchicalScenarioListSc
133
133
  data={filteredScenarios}
134
134
  numColumns={numColumns}
135
135
  showsVerticalScrollIndicator={false}
136
- columnWrapperStyle={numColumns > 1 ? styles.row : undefined}
136
+ columnWrapperStyle={styles.row}
137
137
  renderItem={renderItem}
138
138
  keyExtractor={(item) => item.id}
139
139
  ListEmptyComponent={isLoading ? LoadingComponent : (filteredScenarios.length === 0 ? ListEmptyComponent : null)}
@@ -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}`);
@@ -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,43 +45,92 @@ 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
  );
62
76
 
63
77
  const executeGeneration = useCallback(
64
78
  async (input: TInput) => {
65
- const creditDeducted = await deductCredit(strategy.getCreditCost());
66
- if (!creditDeducted) throw createGenerationError("credits", alertMessages.creditFailed);
79
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
80
+ console.log("[Orchestrator] ----------------------------------------");
81
+ console.log("[Orchestrator] executeGeneration() called");
82
+ }
83
+
84
+ const creditCost = strategy.getCreditCost();
85
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
86
+ console.log("[Orchestrator] Deducting credits:", creditCost);
87
+ }
88
+
89
+ const creditDeducted = await deductCredit(creditCost);
90
+ if (!creditDeducted) {
91
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
92
+ console.log("[Orchestrator] ERROR: Credit deduction failed");
93
+ }
94
+ throw createGenerationError("credits", alertMessages.creditFailed);
95
+ }
96
+
97
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
98
+ console.log("[Orchestrator] Credits deducted successfully");
99
+ }
67
100
 
68
101
  setState((prev) => ({ ...prev, status: "generating" }));
69
- if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[Orchestrator] Starting generation");
102
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
103
+ console.log("[Orchestrator] State: generating - calling strategy.execute()");
104
+ }
70
105
 
71
106
  const result = await strategy.execute(input);
107
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
108
+ console.log("[Orchestrator] strategy.execute() completed");
109
+ }
72
110
 
73
111
  if (strategy.save && userId) {
74
112
  if (isMountedRef.current) setState((prev) => ({ ...prev, status: "saving" }));
113
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
114
+ console.log("[Orchestrator] Saving result to Firestore");
115
+ }
75
116
  try {
76
117
  await strategy.save(result, userId);
118
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
119
+ console.log("[Orchestrator] Result saved successfully");
120
+ }
77
121
  } catch (saveErr) {
122
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
123
+ console.log("[Orchestrator] ERROR: Save failed:", saveErr);
124
+ }
78
125
  throw createGenerationError("save", alertMessages.saveFailed, saveErr instanceof Error ? saveErr : undefined);
79
126
  }
80
127
  }
81
128
 
82
129
  if (isMountedRef.current) setState({ status: "success", isGenerating: false, result, error: null });
83
- if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[Orchestrator] Generation SUCCESS");
130
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
131
+ console.log("[Orchestrator] ✅ Generation SUCCESS");
132
+ console.log("[Orchestrator] ========================================");
133
+ }
84
134
 
85
135
  if (alertMessages.success) showSuccess("Success", alertMessages.success);
86
136
  onSuccess?.(result);
@@ -92,41 +142,81 @@ export const useGenerationOrchestrator = <TInput, TResult>(
92
142
 
93
143
  const generate = useCallback(
94
144
  async (input: TInput) => {
95
- if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[Orchestrator] generate() called");
96
- if (isGeneratingRef.current) return;
145
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
146
+ console.log("[Orchestrator] ========================================");
147
+ console.log("[Orchestrator] generate() called with input:", JSON.stringify(input).substring(0, 200));
148
+ console.log("[Orchestrator] isGenerating:", isGeneratingRef.current);
149
+ console.log("[Orchestrator] userId:", userId);
150
+ }
151
+
152
+ if (isGeneratingRef.current) {
153
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
154
+ console.log("[Orchestrator] BLOCKED: Already generating");
155
+ }
156
+ return;
157
+ }
97
158
 
98
159
  // Create new AbortController for this generation
99
160
  abortControllerRef.current = new AbortController();
100
161
  isGeneratingRef.current = true;
101
162
  setState({ ...INITIAL_STATE, status: "checking", isGenerating: true });
102
163
 
164
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
165
+ console.log("[Orchestrator] State set to 'checking', isGenerating: true");
166
+ }
167
+
103
168
  try {
104
169
  // Check online status inside the try block to avoid dependency on offlineStore.isOnline
105
170
  if (!offlineStore.isOnline) {
171
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
172
+ console.log("[Orchestrator] ERROR: User is offline");
173
+ }
106
174
  throw createGenerationError("network", alertMessages.networkError);
107
175
  }
108
176
 
177
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
178
+ console.log("[Orchestrator] Online check passed");
179
+ }
180
+
109
181
  // Check if aborted
110
182
  if (abortControllerRef.current.signal.aborted) {
183
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
184
+ console.log("[Orchestrator] ERROR: Generation aborted (1)");
185
+ }
111
186
  throw new Error("Generation aborted");
112
187
  }
113
188
 
114
189
  // Pre-validate credits before generation to catch concurrent consumption
115
190
  const creditCost = strategy.getCreditCost();
191
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
192
+ console.log("[Orchestrator] Checking credits - cost:", creditCost);
193
+ }
194
+
116
195
  const hasEnoughCredits = await checkCredits(creditCost);
117
196
  if (!hasEnoughCredits) {
118
197
  if (typeof __DEV__ !== "undefined" && __DEV__) {
119
- console.log("[Orchestrator] Pre-validation: insufficient credits");
198
+ console.log("[Orchestrator] ERROR: Pre-validation failed - insufficient credits");
120
199
  }
121
200
  onCreditsExhausted?.();
122
201
  throw createGenerationError("credits", alertMessages.creditFailed);
123
202
  }
124
203
 
204
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
205
+ console.log("[Orchestrator] Credit check passed");
206
+ }
207
+
125
208
  // Check if aborted before moderation
126
209
  if (abortControllerRef.current.signal.aborted) {
210
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
211
+ console.log("[Orchestrator] ERROR: Generation aborted (2)");
212
+ }
127
213
  throw new Error("Generation aborted");
128
214
  }
129
215
 
216
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
217
+ console.log("[Orchestrator] Starting moderation check");
218
+ }
219
+
130
220
  return await handleModeration({
131
221
  input,
132
222
  moderation,
@@ -161,7 +251,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
161
251
  abortControllerRef.current = null;
162
252
  }
163
253
  },
164
- [offlineStore.isOnline, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
254
+ [offlineStore, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
165
255
  );
166
256
 
167
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