@umituz/react-native-ai-generation-content 1.61.37 → 1.61.38

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 (35) hide show
  1. package/package.json +6 -4
  2. package/src/domain/entities/error.types.ts +1 -0
  3. package/src/domain/entities/polling.types.ts +1 -0
  4. package/src/domains/access-control/hooks/useAIFeatureGate.ts +1 -1
  5. package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +0 -24
  6. package/src/domains/creations/domain/types/creation-filter.ts +1 -1
  7. package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +3 -3
  8. package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +4 -9
  9. package/src/domains/creations/presentation/components/CreationActions.tsx +2 -1
  10. package/src/domains/creations/presentation/components/CreationImageViewer.tsx +1 -1
  11. package/src/domains/creations/presentation/components/CreationRating.tsx +1 -1
  12. package/src/domains/creations/presentation/components/CreationsGrid.tsx +1 -1
  13. package/src/domains/creations/presentation/components/CreationsHomeCard.tsx +1 -1
  14. package/src/domains/creations/presentation/components/FilterSheets.tsx +2 -2
  15. package/src/domains/creations/presentation/components/GalleryEmptyStates.tsx +1 -1
  16. package/src/domains/creations/presentation/components/GalleryHeader.tsx +2 -2
  17. package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +1 -1
  18. package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +6 -14
  19. package/src/domains/generation/infrastructure/flow/step-builders.ts +0 -1
  20. package/src/domains/generation/infrastructure/flow/useFlow.ts +3 -0
  21. package/src/domains/generation/wizard/presentation/hooks/generation-result.utils.ts +14 -9
  22. package/src/index.ts +0 -3
  23. package/src/infrastructure/constants/index.ts +0 -6
  24. package/src/infrastructure/logging/logger.ts +0 -11
  25. package/src/infrastructure/services/job-poller.service.ts +17 -8
  26. package/src/infrastructure/services/video-feature-executor.service.ts +2 -2
  27. package/src/infrastructure/utils/error-classifier.util.ts +1 -1
  28. package/src/infrastructure/utils/result-validator.util.ts +3 -1
  29. package/src/infrastructure/utils/status-checker.util.ts +13 -10
  30. package/src/infrastructure/utils/url-extractor/media-extractors.ts +6 -4
  31. package/src/infrastructure/validation/input-validator.ts +5 -0
  32. package/src/presentation/components/AIGenerationForm.tsx +1 -2
  33. package/src/presentation/components/PhotoUploadCard/useBorderColor.ts +1 -1
  34. package/src/presentation/types/result-config.types.ts +7 -0
  35. package/src/utils/arrayUtils.ts +0 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.61.37",
3
+ "version": "1.61.38",
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",
@@ -65,12 +65,14 @@
65
65
  "@tanstack/react-query": "^5.66.7",
66
66
  "@tanstack/react-query-persist-client": "^5.66.7",
67
67
  "@types/react": "~19.1.10",
68
- "@typescript-eslint/eslint-plugin": "^8.0.0",
69
- "@typescript-eslint/parser": "^8.0.0",
68
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
69
+ "@typescript-eslint/parser": "^8.54.0",
70
70
  "@umituz/react-native-design-system": "^4.23.42",
71
71
  "@umituz/react-native-firebase": "^1.13.87",
72
72
  "@umituz/react-native-subscription": "^2.27.23",
73
- "eslint": "^9.0.0",
73
+ "eslint": "^9.39.2",
74
+ "eslint-plugin-react": "^7.37.5",
75
+ "eslint-plugin-react-hooks": "^7.0.1",
74
76
  "expo-apple-authentication": "^8.0.8",
75
77
  "expo-application": "^7.0.8",
76
78
  "expo-auth-session": "^5.0.0",
@@ -19,6 +19,7 @@ export interface AIErrorInfo {
19
19
  messageKey: string;
20
20
  originalError?: unknown;
21
21
  statusCode?: number;
22
+ retryable?: boolean;
22
23
  }
23
24
 
24
25
  export interface AIErrorMessages {
@@ -9,6 +9,7 @@ export interface PollingConfig {
9
9
  maxIntervalMs: number;
10
10
  backoffMultiplier: number;
11
11
  maxTotalTimeMs?: number;
12
+ maxConsecutiveErrors?: number;
12
13
  }
13
14
 
14
15
  export const DEFAULT_POLLING_CONFIG: PollingConfig = {
@@ -65,7 +65,7 @@ export function useAIFeatureGate(
65
65
  // Configure feature gate from subscription package
66
66
  const { requireFeature: requireFeatureFromPackage } = useFeatureGate({
67
67
  isAuthenticated,
68
- onShowAuthModal: (cb) => showAuthModal(cb),
68
+ onShowAuthModal: (cb?: () => void) => showAuthModal(cb),
69
69
  hasSubscription: isPremium,
70
70
  creditBalance,
71
71
  requiredCredits: creditCost,
@@ -91,30 +91,6 @@ function containsPromptInjection(content: string): boolean {
91
91
  });
92
92
  }
93
93
 
94
- // Kept for reference but no longer used directly - using safer functions above
95
- const MALICIOUS_CODE_PATTERNS = [
96
- /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
97
- /javascript:/gi,
98
- /on\w+\s*=/gi,
99
- ];
100
-
101
- const PROMPT_INJECTION_PATTERNS = [
102
- /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|rules?)/gi,
103
- /disregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?)/gi,
104
- /forget\s+(all\s+)?(previous|prior|your)\s+(instructions?|prompts?|rules?)/gi,
105
- /you\s+are\s+now\s+(a|an)\s+/gi,
106
- /act\s+as\s+(if|though)\s+you/gi,
107
- /pretend\s+(you\s+are|to\s+be)/gi,
108
- /bypass\s+(your\s+)?(safety|content|moderation)/gi,
109
- /override\s+(your\s+)?(restrictions?|limitations?|rules?)/gi,
110
- /jailbreak/gi,
111
- /DAN\s*mode/gi,
112
- /developer\s+mode\s+(enabled|on|activated)/gi,
113
- /system\s*:\s*/gi,
114
- /\[system\]/gi,
115
- /<<\s*sys\s*>>/gi,
116
- ];
117
-
118
94
  class TextModerator extends BaseModerator {
119
95
  private maxLength = DEFAULT_MAX_TEXT_LENGTH;
120
96
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { CreationTypeId, CreationStatus, CreationCategory } from "./creation-types";
7
+ import { getCategoryForCreation } from "./creation-categories";
7
8
 
8
9
  /**
9
10
  * Filter options for querying creations
@@ -118,7 +119,6 @@ export function calculateCreationStats(
118
119
  }
119
120
 
120
121
  // Calculate category counts based on OUTPUT content (most reliable)
121
- const { getCategoryForCreation } = require("./creation-categories");
122
122
  for (const creation of creations) {
123
123
  const category = getCategoryForCreation(creation) as Exclude<CreationCategory, "all">;
124
124
  stats.byCategory[category]++;
@@ -52,7 +52,7 @@ export class CreationsFetcher {
52
52
  });
53
53
 
54
54
  // Filter out soft-deleted creations
55
- return allCreations.filter((creation) => !creation.deletedAt);
55
+ return allCreations.filter((creation: Creation) => !creation.deletedAt);
56
56
  } catch (error) {
57
57
  if (__DEV__) {
58
58
 
@@ -132,7 +132,7 @@ export class CreationsFetcher {
132
132
  return creation;
133
133
  });
134
134
 
135
- const filtered = allCreations.filter((c) => !c.deletedAt);
135
+ const filtered = allCreations.filter((c: Creation) => !c.deletedAt);
136
136
 
137
137
  if (__DEV__) {
138
138
  console.log("[CreationsFetcher] Realtime update:", filtered.length);
@@ -140,7 +140,7 @@ export class CreationsFetcher {
140
140
 
141
141
  onData(filtered);
142
142
  },
143
- (error) => {
143
+ (error: Error) => {
144
144
  if (__DEV__) {
145
145
  console.error("[CreationsFetcher] subscribeToAll() ERROR", error);
146
146
  }
@@ -43,22 +43,17 @@ export class CreationsRepository
43
43
  collectionName: string,
44
44
  options?: RepositoryOptions,
45
45
  ) {
46
-
46
+
47
47
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - Constructor start");
48
48
  super();
49
49
 
50
50
  const documentMapper = options?.documentMapper ?? mapDocumentToCreation;
51
51
 
52
-
53
- if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - Getting db");
54
- const db = this.getDb();
55
-
56
- if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - db:", db ? "available" : "null");
57
-
58
- this.pathResolver = new FirestorePathResolver(collectionName, db);
52
+ // Initialize with default database (will be resolved by FirestorePathResolver)
53
+ this.pathResolver = new FirestorePathResolver(collectionName, null);
59
54
  this.fetcher = new CreationsFetcher(this.pathResolver, documentMapper);
60
55
  this.writer = new CreationsWriter(this.pathResolver);
61
-
56
+
62
57
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - Constructor end");
63
58
  }
64
59
 
@@ -8,6 +8,7 @@ import {
8
8
  View,
9
9
  StyleSheet,
10
10
  TouchableOpacity,
11
+ type GestureResponderEvent,
11
12
  } from "react-native";
12
13
  import {
13
14
  useAppDesignTokens,
@@ -99,7 +100,7 @@ export function CreationActions({
99
100
  action.filled && styles.actionButtonFilled,
100
101
  action.disabled && styles.actionButtonDisabled,
101
102
  ]}
102
- onPress={(e) => {
103
+ onPress={(e: GestureResponderEvent) => {
103
104
  e.stopPropagation();
104
105
  action.onPress();
105
106
  }}
@@ -21,7 +21,7 @@ export const CreationImageViewer: React.FC<CreationImageViewerProps> = ({
21
21
  onDismiss,
22
22
  onIndexChange,
23
23
  onImageEdit,
24
- }) => {
24
+ }: CreationImageViewerProps) => {
25
25
  const handleImageChange = useCallback(async (uri: string, idx: number) => {
26
26
  const creation = creations[idx];
27
27
  if (creation && onImageEdit) {
@@ -16,7 +16,7 @@ export const CreationRating: React.FC<CreationRatingProps> = ({
16
16
  size = 32,
17
17
  onRate,
18
18
  readonly = false,
19
- }) => {
19
+ }: CreationRatingProps) => {
20
20
  const tokens = useAppDesignTokens();
21
21
 
22
22
  return (
@@ -101,7 +101,7 @@ export function CreationsGrid<T extends CreationCardData>({
101
101
  <FlatList
102
102
  data={creations}
103
103
  renderItem={renderItem}
104
- keyExtractor={(item) => item.id}
104
+ keyExtractor={(item: T) => item.id}
105
105
  ListHeaderComponent={ListHeaderComponent}
106
106
  ListEmptyComponent={ListEmptyComponent}
107
107
  contentContainerStyle={[
@@ -165,7 +165,7 @@ export function CreationsHomeCard({
165
165
  <FlatList
166
166
  data={displayItems}
167
167
  renderItem={renderItem}
168
- keyExtractor={(item) => item.id}
168
+ keyExtractor={(item: Creation) => item.id}
169
169
  horizontal
170
170
  showsHorizontalScrollIndicator={false}
171
171
  contentContainerStyle={styles.thumbnailList}
@@ -27,7 +27,7 @@ export const StatusFilterSheet: React.FC<FilterSheetConfig> = ({
27
27
  onClear,
28
28
  title,
29
29
  clearLabel
30
- }) => (
30
+ }: FilterSheetConfig) => (
31
31
  <FilterSheet
32
32
  visible={visible}
33
33
  onClose={onClose}
@@ -49,7 +49,7 @@ export const MediaFilterSheet: React.FC<FilterSheetConfig> = ({
49
49
  onClear,
50
50
  title,
51
51
  clearLabel
52
- }) => (
52
+ }: FilterSheetConfig) => (
53
53
  <FilterSheet
54
54
  visible={visible}
55
55
  onClose={onClose}
@@ -76,7 +76,7 @@ export function GalleryEmptyStates({
76
76
  if (isLoading && (!creations || creations?.length === 0)) {
77
77
  return (
78
78
  <View style={styles.skeletonContainer}>
79
- {[1, 2, 3].map((i) => (
79
+ {[1, 2, 3].map((i: number) => (
80
80
  <CreationCardSkeleton key={i} tokens={tokens} />
81
81
  ))}
82
82
  </View>
@@ -28,7 +28,7 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
28
28
  filterButtons = [],
29
29
  showFilter = true,
30
30
  style,
31
- }) => {
31
+ }: GalleryHeaderProps) => {
32
32
  const tokens = useAppDesignTokens();
33
33
  const styles = useStyles(tokens);
34
34
 
@@ -44,7 +44,7 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
44
44
  </View>
45
45
  {showFilter && filterButtons.length > 0 && (
46
46
  <View style={styles.filterRow}>
47
- {filterButtons.map((btn) => (
47
+ {filterButtons.map((btn: FilterButtonConfig) => (
48
48
  <TouchableOpacity
49
49
  key={btn.id}
50
50
  onPress={() => {
@@ -27,7 +27,7 @@ export function useCreationsFilter({
27
27
  }: UseCreationsFilterProps): UseCreationsFilterReturn {
28
28
 
29
29
  const filtered = useMemo(() => {
30
- if (!creations) return [];
30
+ if (!creations || creations.length === 0) return [];
31
31
 
32
32
  return creations.filter((creation) => {
33
33
  // Status filter
@@ -8,7 +8,7 @@
8
8
  import { useEffect, useRef, useCallback, useMemo } from "react";
9
9
  import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
10
10
  import { QUEUE_STATUS, CREATION_STATUS } from "../../../../domain/constants/queue-status.constants";
11
- import { GALLERY_POLL_INTERVAL_MS } from "../../../../infrastructure/constants/polling.constants";
11
+ import { DEFAULT_POLL_INTERVAL_MS } from "../../../../infrastructure/constants/polling.constants";
12
12
  import {
13
13
  extractResultUrl,
14
14
  type FalResult,
@@ -42,16 +42,6 @@ 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
- // Find creations that need polling - use Set for O(1) lookups
46
- const processingJobIds = useMemo(
47
- () => new Set(
48
- creations
49
- .filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
50
- .map((c) => c.id)
51
- ),
52
- [creations],
53
- );
54
-
55
45
  const processingJobs = useMemo(
56
46
  () => creations.filter(
57
47
  (c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
@@ -126,15 +116,17 @@ export function useProcessingJobsPoller(
126
116
  // Set up interval polling
127
117
  intervalRef.current = setInterval(() => {
128
118
  processingJobs.forEach((job) => void pollJob(job));
129
- }, POLL_INTERVAL_MS);
119
+ }, DEFAULT_POLL_INTERVAL_MS);
130
120
 
131
121
  return () => {
122
+ // Clear polling set first to prevent new operations
123
+ pollingRef.current.clear();
124
+
125
+ // Then clear interval
132
126
  if (intervalRef.current) {
133
127
  clearInterval(intervalRef.current);
134
128
  intervalRef.current = null;
135
129
  }
136
- // Clear polling set to prevent memory leak
137
- pollingRef.current.clear();
138
130
  };
139
131
  }, [enabled, userId, processingJobs, pollJob]);
140
132
 
@@ -14,7 +14,6 @@ import type {
14
14
  * Build steps from scenario configuration
15
15
  */
16
16
  export const buildStepsFromScenario = (
17
- scenarioId: string,
18
17
  config: ScenarioStepConfig,
19
18
  ): DynamicStepDefinition[] => {
20
19
  const steps: DynamicStepDefinition[] = [];
@@ -70,6 +70,9 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
70
70
  };
71
71
 
72
72
  const store = storeRef.current;
73
+ if (!store) {
74
+ throw new Error("Flow store not initialized");
75
+ }
73
76
  const state = store();
74
77
  const totalSteps = config.steps.length;
75
78
 
@@ -31,8 +31,10 @@ function checkForErrors(result: FalResult): void {
31
31
  // Check for FAL API error format: {detail: [{msg, type}]}
32
32
  if (result.detail && Array.isArray(result.detail) && result.detail.length > 0) {
33
33
  const firstError = result.detail[0];
34
- const errorType = firstError?.type || "unknown";
35
- const errorMsg = firstError?.msg || "Generation failed";
34
+ if (!firstError) return;
35
+
36
+ const errorType = firstError.type || "unknown";
37
+ const errorMsg = firstError.msg || "Generation failed";
36
38
 
37
39
  // Map error type to translation key
38
40
  if (errorType === "content_policy_violation") {
@@ -47,7 +49,7 @@ function checkForErrors(result: FalResult): void {
47
49
  }
48
50
 
49
51
  // Check for simple error field
50
- if (result.error) {
52
+ if (result.error && typeof result.error === "string" && result.error.length > 0) {
51
53
  throw new Error(result.error);
52
54
  }
53
55
  }
@@ -62,25 +64,28 @@ export function extractResultUrl(result: FalResult): GenerationUrls {
62
64
  checkForErrors(result);
63
65
 
64
66
  // Video result
65
- if (result.video?.url) {
67
+ if (result.video?.url && typeof result.video.url === "string") {
66
68
  return { videoUrl: result.video.url };
67
69
  }
68
70
 
69
71
  // Output URL (some models return direct URL)
70
- if (typeof result.output === "string" && result.output.startsWith("http")) {
72
+ if (typeof result.output === "string" && result.output.length > 0 && result.output.startsWith("http")) {
71
73
  if (result.output.includes(".mp4") || result.output.includes("video")) {
72
74
  return { videoUrl: result.output };
73
75
  }
74
76
  return { imageUrl: result.output };
75
77
  }
76
78
 
77
- // Images array (most image models)
78
- if (result.images?.[0]?.url) {
79
- return { imageUrl: result.images[0].url };
79
+ // Images array (most image models) with bounds checking
80
+ if (result.images && Array.isArray(result.images) && result.images.length > 0) {
81
+ const firstImage = result.images[0];
82
+ if (firstImage?.url && typeof firstImage.url === "string") {
83
+ return { imageUrl: firstImage.url };
84
+ }
80
85
  }
81
86
 
82
87
  // Single image
83
- if (result.image?.url) {
88
+ if (result.image?.url && typeof result.image.url === "string") {
84
89
  return { imageUrl: result.image.url };
85
90
  }
86
91
 
package/src/index.ts CHANGED
@@ -17,6 +17,3 @@ export * from "./exports/domains";
17
17
 
18
18
  // Features
19
19
  export * from "./exports/features";
20
-
21
- // Utils
22
- export { distinctBy } from "./utils/arrayUtils";
@@ -13,9 +13,3 @@ export * from "./content.constants";
13
13
 
14
14
  // Storage Constants
15
15
  export * from "./storage.constants";
16
-
17
- /** Video generation timeout in milliseconds (5 minutes) - @deprecated Use DEFAULT_MAX_POLL_TIME_MS instead */
18
- export const VIDEO_TIMEOUT_MS = 300000;
19
-
20
- /** Maximum consecutive transient errors before failing - @deprecated Use DEFAULT_MAX_CONSECUTIVE_ERRORS instead */
21
- export const MAX_TRANSIENT_ERRORS = 5;
@@ -52,16 +52,6 @@ class Logger {
52
52
  }
53
53
  }
54
54
 
55
- private formatMessage(entry: LogEntry): string {
56
- const levelName = LogLevel[entry.level];
57
- const timestamp = new Date(entry.timestamp).toISOString();
58
- const contextStr = entry.context
59
- ? ` ${JSON.stringify(entry.context)}`
60
- : "";
61
- const errorStr = entry.error ? ` ${entry.error.message}` : "";
62
- return `[${timestamp}] [${levelName}] ${entry.message}${contextStr}${errorStr}`;
63
- }
64
-
65
55
  debug(message: string, context?: LogContext): void {
66
56
  if (!this.shouldLog(LogLevel.DEBUG)) return;
67
57
 
@@ -182,4 +172,3 @@ export function setProductionMode(): void {
182
172
  logger.setMinLevel(LogLevel.WARN);
183
173
  }
184
174
 
185
- export type { LogEntry, LogContext };
@@ -28,9 +28,10 @@ export async function pollJob<T = unknown>(
28
28
  } = options;
29
29
 
30
30
  const pollingConfig = { ...DEFAULT_POLLING_CONFIG, ...config };
31
- const { maxAttempts, maxTotalTimeMs } = pollingConfig;
31
+ const { maxAttempts, maxTotalTimeMs, maxConsecutiveErrors } = pollingConfig;
32
32
 
33
33
  const startTime = Date.now();
34
+ let consecutiveTransientErrors = 0;
34
35
 
35
36
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
36
37
  // Check total time limit
@@ -97,13 +98,21 @@ export async function pollJob<T = unknown>(
97
98
  elapsedMs: Date.now() - startTime,
98
99
  };
99
100
  }
100
- } catch (error) {
101
- return {
102
- success: false,
103
- error: error instanceof Error ? error : new Error(String(error)),
104
- attempts: attempt + 1,
105
- elapsedMs: Date.now() - startTime,
106
- };
101
+ } catch {
102
+ consecutiveTransientErrors++;
103
+
104
+ // Check if we've hit max consecutive transient errors
105
+ if (maxConsecutiveErrors && consecutiveTransientErrors >= maxConsecutiveErrors) {
106
+ return {
107
+ success: false,
108
+ error: new Error(`Too many consecutive errors (${consecutiveTransientErrors})`),
109
+ attempts: attempt + 1,
110
+ elapsedMs: Date.now() - startTime,
111
+ };
112
+ }
113
+
114
+ // Continue polling on transient errors
115
+ continue;
107
116
  }
108
117
  }
109
118
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { extractErrorMessage, checkFalApiError, validateProvider, prepareVideoInputData } from "../utils";
8
8
  import { extractVideoResult } from "../utils/url-extractor";
9
- import { VIDEO_TIMEOUT_MS } from "../constants";
9
+ import { DEFAULT_MAX_POLL_TIME_MS } from "../constants";
10
10
  import type { VideoFeatureType } from "../../domain/interfaces";
11
11
  import type { ExecuteVideoFeatureOptions, VideoFeatureResult, VideoFeatureRequest } from "./video-feature-executor.types";
12
12
 
@@ -38,7 +38,7 @@ export async function executeVideoFeature(
38
38
  const input = provider.buildVideoFeatureInput(featureType, inputData);
39
39
 
40
40
  const result = await provider.subscribe(model, input, {
41
- timeoutMs: VIDEO_TIMEOUT_MS,
41
+ timeoutMs: DEFAULT_MAX_POLL_TIME_MS,
42
42
  onQueueUpdate: (status) => onStatusChange?.(status.status),
43
43
  });
44
44
 
@@ -143,7 +143,7 @@ export function classifyError(error: unknown): AIErrorInfo {
143
143
 
144
144
  export function isTransientError(error: unknown): boolean {
145
145
  const info = classifyError(error);
146
- return info.retryable;
146
+ return info.retryable ?? false;
147
147
  }
148
148
 
149
149
  export function isPermanentError(error: unknown): boolean {
@@ -77,11 +77,13 @@ export function validateResult(
77
77
 
78
78
  if (typeof value === "object" && value !== null) {
79
79
  const nested = value as Record<string, unknown>;
80
+ // Cache Object.keys result for performance
81
+ const nestedKeys = Object.keys(nested);
80
82
  return !!(
81
83
  nested.url ||
82
84
  nested.image_url ||
83
85
  nested.video_url ||
84
- Object.keys(nested).length > 0
86
+ nestedKeys.length > 0
85
87
  );
86
88
  }
87
89
 
@@ -50,7 +50,7 @@ export function checkStatusForErrors(
50
50
  safeString(status, "message");
51
51
  const hasStatusError = statusError.length > 0;
52
52
 
53
- // Check logs array for ERROR/FATAL level logs
53
+ // Check logs array for ERROR/FATAL level logs with bounds checking
54
54
  const rawLogs = (status as JobStatus)?.logs;
55
55
  const logs = Array.isArray(rawLogs) ? rawLogs : [];
56
56
  const errorLogs = logs.filter((log: AILogEntry) => {
@@ -59,14 +59,17 @@ export function checkStatusForErrors(
59
59
  });
60
60
  const hasErrorLog = errorLogs.length > 0;
61
61
 
62
- // Extract error message from logs
63
- const errorLogMessage =
64
- errorLogs.length > 0
65
- ? (errorLogs[0] as AILogEntry & { text?: string; content?: string })
66
- ?.message ||
67
- (errorLogs[0] as AILogEntry & { text?: string })?.text ||
68
- (errorLogs[0] as AILogEntry & { content?: string })?.content
69
- : undefined;
62
+ // Extract error message from logs with safer access
63
+ let errorLogMessage: string | undefined;
64
+ if (errorLogs.length > 0) {
65
+ const firstErrorLog = errorLogs[0];
66
+ if (firstErrorLog && typeof firstErrorLog === "object") {
67
+ errorLogMessage =
68
+ safeString(firstErrorLog, "message") ||
69
+ safeString(firstErrorLog, "text") ||
70
+ safeString(firstErrorLog, "content");
71
+ }
72
+ }
70
73
 
71
74
  // Combine error messages
72
75
  const errorMessage = statusError || errorLogMessage;
@@ -78,7 +81,7 @@ export function checkStatusForErrors(
78
81
  return {
79
82
  status: statusString,
80
83
  hasError: hasStatusError || hasErrorLog,
81
- errorMessage: errorMessage ? String(errorMessage) : undefined,
84
+ errorMessage: errorMessage && errorMessage.length > 0 ? errorMessage : undefined,
82
85
  shouldStop,
83
86
  };
84
87
  }
@@ -47,14 +47,16 @@ export function extractImageUrls(result: unknown): string[] {
47
47
  return urls;
48
48
  }
49
49
 
50
- // Check images array
51
- if (Array.isArray(resultObj.images)) {
50
+ // Check images array with bounds checking
51
+ if (Array.isArray(resultObj.images) && resultObj.images.length > 0) {
52
52
  for (const img of resultObj.images) {
53
+ if (!img) continue; // Skip null/undefined items
54
+
53
55
  if (typeof img === "string" && img.length > 0) {
54
56
  urls.push(img);
55
- } else if (img && typeof img === "object") {
57
+ } else if (typeof img === "object") {
56
58
  const imgObj = img as Record<string, unknown>;
57
- if (typeof imgObj.url === "string") {
59
+ if (typeof imgObj.url === "string" && imgObj.url.length > 0) {
58
60
  urls.push(imgObj.url);
59
61
  }
60
62
  }
@@ -50,7 +50,12 @@ export function sanitizeString(input: unknown): string {
50
50
  .trim()
51
51
  .replace(/[<>]/g, "") // Remove potential HTML tags
52
52
  .replace(/javascript:/gi, "") // Remove javascript: protocol
53
+ .replace(/data:/gi, "") // Remove data: protocol
54
+ .replace(/vbscript:/gi, "") // Remove vbscript: protocol
53
55
  .replace(/on\w+\s*=/gi, "") // Remove event handlers
56
+ .replace(/--/g, "") // Remove SQL comment sequences
57
+ .replace(/;\s*drop\s+/gi, "") // Remove SQL injection attempts
58
+ .replace(/['"\\]/g, "") // Remove quotes and backslashes
54
59
  .slice(0, 10000); // Limit length
55
60
  }
56
61
 
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from "react";
1
+ import React from "react";
2
2
  import { TouchableOpacity, StyleSheet } from "react-native";
3
3
  import {
4
4
  AtomicText,
@@ -6,7 +6,6 @@ import {
6
6
  useAppDesignTokens,
7
7
  } from "@umituz/react-native-design-system";
8
8
 
9
- declare const __DEV__: boolean;
10
9
  import { StyleSelector } from "./selectors/StyleSelector";
11
10
  import { DurationSelector } from "./selectors/DurationSelector";
12
11
  import { AspectRatioSelector } from "./selectors/AspectRatioSelector";
@@ -27,5 +27,5 @@ export function useBorderColor({
27
27
  if (isValid === true) return tokens.colors.success;
28
28
  if (isValid === false) return tokens.colors.error;
29
29
  return tokens.colors.borderLight;
30
- }, [isValidating, isValid, showValidationStatus, tokens]);
30
+ }, [isValidating, isValid, showValidationStatus, tokens.colors.borderLight, tokens.colors.primary, tokens.colors.success, tokens.colors.error]);
31
31
  }
@@ -78,6 +78,7 @@ export interface ResultActionButton {
78
78
  export interface ResultActionsConfig {
79
79
  share?: ResultActionButton;
80
80
  save?: ResultActionButton;
81
+ retry?: ResultActionButton;
81
82
  layout?: "horizontal" | "vertical" | "grid";
82
83
  buttonSpacing?: number;
83
84
  spacing?: {
@@ -168,6 +169,12 @@ export const DEFAULT_RESULT_CONFIG: ResultConfig = {
168
169
  variant: "secondary",
169
170
  position: "bottom",
170
171
  },
172
+ retry: {
173
+ enabled: true,
174
+ icon: "refresh",
175
+ variant: "outline",
176
+ position: "bottom",
177
+ },
171
178
  layout: "horizontal",
172
179
  buttonSpacing: 10,
173
180
  spacing: {
@@ -1,22 +0,0 @@
1
- /**
2
- * Deduplicates an array of objects based on a specific key.
3
- * Keeps the first occurrence of each item with a unique key.
4
- *
5
- * @param array The array to deduplicate
6
- * @param keyFn A function that returns the unique key for each item
7
- * @returns A new array with duplicate items removed
8
- */
9
- export const distinctBy = <T>(array: readonly T[], keyFn: (item: T) => string | number): T[] => {
10
- const seen = new Set<string | number>();
11
- const result: T[] = [];
12
-
13
- for (const item of array) {
14
- const key = keyFn(item);
15
- if (!seen.has(key)) {
16
- seen.add(key);
17
- result.push(item);
18
- }
19
- }
20
-
21
- return result;
22
- };