@umituz/react-native-ai-generation-content 1.61.32 → 1.61.34

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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/domain/entities/error.types.ts +0 -1
  3. package/src/domain/entities/job.types.ts +0 -4
  4. package/src/domain/entities/polling.types.ts +1 -3
  5. package/src/domain/interfaces/app-services.interface.ts +20 -2
  6. package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts +2 -1
  7. package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +84 -4
  8. package/src/domains/content-moderation/infrastructure/services/pattern-matcher.service.ts +85 -2
  9. package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +102 -19
  10. package/src/domains/creations/presentation/hooks/useAdvancedFilter.ts +13 -4
  11. package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +10 -9
  12. package/src/domains/face-detection/presentation/hooks/useFaceDetection.ts +1 -1
  13. package/src/domains/generation/infrastructure/flow/useFlow.ts +11 -6
  14. package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +2 -3
  15. package/src/exports/infrastructure.ts +24 -1
  16. package/src/exports/presentation.ts +1 -1
  17. package/src/features/image-to-video/presentation/components/index.ts +0 -4
  18. package/src/index.ts +0 -4
  19. package/src/infrastructure/constants/index.ts +14 -2
  20. package/src/infrastructure/constants/polling.constants.ts +34 -0
  21. package/src/infrastructure/constants/storage.constants.ts +25 -0
  22. package/src/infrastructure/constants/validation.constants.ts +40 -0
  23. package/src/infrastructure/logging/logger.ts +185 -0
  24. package/src/infrastructure/services/job-poller.service.ts +13 -28
  25. package/src/infrastructure/utils/status-checker.util.ts +27 -7
  26. package/src/infrastructure/validation/input-validator.ts +406 -0
  27. package/src/presentation/components/AIGenerationForm.tsx +0 -11
  28. package/src/presentation/components/ErrorBoundary.tsx +141 -0
  29. package/src/presentation/components/index.ts +1 -0
  30. package/src/presentation/hooks/use-background-generation.ts +0 -12
  31. package/src/presentation/hooks/useAIFeatureCallbacks.ts +3 -4
  32. package/src/presentation/types/result-config.types.ts +0 -7
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Input Validation Utilities
3
+ * Provides comprehensive input validation for security and data integrity
4
+ */
5
+
6
+ import {
7
+ MAX_PROMPT_LENGTH,
8
+ MIN_PROMPT_LENGTH,
9
+ MAX_USER_ID_LENGTH,
10
+ MAX_CREATION_ID_LENGTH,
11
+ } from "../constants/validation.constants";
12
+
13
+ /**
14
+ * Validation result type
15
+ */
16
+ export interface ValidationResult {
17
+ readonly isValid: boolean;
18
+ readonly errors: readonly string[];
19
+ }
20
+
21
+ /**
22
+ * String validation options
23
+ */
24
+ export interface StringValidationOptions {
25
+ readonly minLength?: number;
26
+ readonly maxLength?: number;
27
+ readonly pattern?: RegExp;
28
+ readonly allowedCharacters?: RegExp;
29
+ readonly trim?: boolean;
30
+ }
31
+
32
+ /**
33
+ * Numeric validation options
34
+ */
35
+ export interface NumericValidationOptions {
36
+ readonly min?: number;
37
+ readonly max?: number;
38
+ readonly integer?: boolean;
39
+ }
40
+
41
+ /**
42
+ * Sanitizes user input to prevent XSS and injection attacks
43
+ */
44
+ export function sanitizeString(input: unknown): string {
45
+ if (typeof input !== "string") {
46
+ return "";
47
+ }
48
+
49
+ return input
50
+ .trim()
51
+ .replace(/[<>]/g, "") // Remove potential HTML tags
52
+ .replace(/javascript:/gi, "") // Remove javascript: protocol
53
+ .replace(/on\w+\s*=/gi, "") // Remove event handlers
54
+ .slice(0, 10000); // Limit length
55
+ }
56
+
57
+ /**
58
+ * Validates a string input against provided rules
59
+ */
60
+ export function validateString(
61
+ input: unknown,
62
+ options: StringValidationOptions = {}
63
+ ): ValidationResult {
64
+ const errors: string[] = [];
65
+
66
+ // Check if input is a string
67
+ if (typeof input !== "string") {
68
+ return {
69
+ isValid: false,
70
+ errors: ["Input must be a string"],
71
+ };
72
+ }
73
+
74
+ let value = options.trim !== false ? input.trim() : input;
75
+
76
+ // Check min length
77
+ if (options.minLength !== undefined && value.length < options.minLength) {
78
+ errors.push(`Input must be at least ${options.minLength} characters`);
79
+ }
80
+
81
+ // Check max length
82
+ if (options.maxLength !== undefined && value.length > options.maxLength) {
83
+ errors.push(`Input must be at most ${options.maxLength} characters`);
84
+ }
85
+
86
+ // Check pattern
87
+ if (options.pattern && !options.pattern.test(value)) {
88
+ errors.push("Input format is invalid");
89
+ }
90
+
91
+ // Check allowed characters
92
+ if (options.allowedCharacters && !options.allowedCharacters.test(value)) {
93
+ errors.push("Input contains invalid characters");
94
+ }
95
+
96
+ return {
97
+ isValid: errors.length === 0,
98
+ errors,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Validates a numeric input
104
+ */
105
+ export function validateNumber(
106
+ input: unknown,
107
+ options: NumericValidationOptions = {}
108
+ ): ValidationResult {
109
+ const errors: string[] = [];
110
+
111
+ // Check if input is a number
112
+ if (typeof input !== "number" || isNaN(input)) {
113
+ return {
114
+ isValid: false,
115
+ errors: ["Input must be a number"],
116
+ };
117
+ }
118
+
119
+ // Check if integer
120
+ if (options.integer && !Number.isInteger(input)) {
121
+ errors.push("Input must be an integer");
122
+ }
123
+
124
+ // Check min value
125
+ if (options.min !== undefined && input < options.min) {
126
+ errors.push(`Input must be at least ${options.min}`);
127
+ }
128
+
129
+ // Check max value
130
+ if (options.max !== undefined && input > options.max) {
131
+ errors.push(`Input must be at most ${options.max}`);
132
+ }
133
+
134
+ return {
135
+ isValid: errors.length === 0,
136
+ errors,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Validates URL format
142
+ */
143
+ export function validateURL(input: unknown): ValidationResult {
144
+ if (typeof input !== "string") {
145
+ return {
146
+ isValid: false,
147
+ errors: ["URL must be a string"],
148
+ };
149
+ }
150
+
151
+ try {
152
+ const url = new URL(input);
153
+
154
+ // Only allow http and https protocols
155
+ if (!["http:", "https:"].includes(url.protocol)) {
156
+ return {
157
+ isValid: false,
158
+ errors: ["Only HTTP and HTTPS protocols are allowed"],
159
+ };
160
+ }
161
+
162
+ return { isValid: true, errors: [] };
163
+ } catch {
164
+ return {
165
+ isValid: false,
166
+ errors: ["Invalid URL format"],
167
+ };
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Validates email format
173
+ */
174
+ export function validateEmail(input: unknown): ValidationResult {
175
+ if (typeof input !== "string") {
176
+ return {
177
+ isValid: false,
178
+ errors: ["Email must be a string"],
179
+ };
180
+ }
181
+
182
+ // Basic email validation regex
183
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
184
+
185
+ if (!emailRegex.test(input)) {
186
+ return {
187
+ isValid: false,
188
+ errors: ["Invalid email format"],
189
+ };
190
+ }
191
+
192
+ return { isValid: true, errors: [] };
193
+ }
194
+
195
+ /**
196
+ * Validates base64 string
197
+ */
198
+ export function validateBase64(input: unknown): ValidationResult {
199
+ if (typeof input !== "string") {
200
+ return {
201
+ isValid: false,
202
+ errors: ["Input must be a string"],
203
+ };
204
+ }
205
+
206
+ // Check if it's a valid base64 string
207
+ const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
208
+
209
+ if (!base64Regex.test(input)) {
210
+ return {
211
+ isValid: false,
212
+ errors: ["Invalid base64 format"],
213
+ };
214
+ }
215
+
216
+ // Check if length is valid (must be multiple of 4)
217
+ if (input.length % 4 !== 0) {
218
+ return {
219
+ isValid: false,
220
+ errors: ["Base64 string length must be a multiple of 4"],
221
+ };
222
+ }
223
+
224
+ return { isValid: true, errors: [] };
225
+ }
226
+
227
+ /**
228
+ * Validates object structure
229
+ */
230
+ export function validateObject(
231
+ input: unknown,
232
+ requiredFields: readonly string[] = []
233
+ ): ValidationResult {
234
+ const errors: string[] = [];
235
+
236
+ if (typeof input !== "object" || input === null) {
237
+ return {
238
+ isValid: false,
239
+ errors: ["Input must be an object"],
240
+ };
241
+ }
242
+
243
+ // Check required fields
244
+ for (const field of requiredFields) {
245
+ if (!(field in input)) {
246
+ errors.push(`Missing required field: ${field}`);
247
+ }
248
+ }
249
+
250
+ return {
251
+ isValid: errors.length === 0,
252
+ errors,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Validates array input
258
+ */
259
+ export function validateArray(
260
+ input: unknown,
261
+ options: {
262
+ readonly minLength?: number;
263
+ readonly maxLength?: number;
264
+ readonly itemType?: "string" | "number" | "object";
265
+ } = {}
266
+ ): ValidationResult {
267
+ const errors: string[] = [];
268
+
269
+ if (!Array.isArray(input)) {
270
+ return {
271
+ isValid: false,
272
+ errors: ["Input must be an array"],
273
+ };
274
+ }
275
+
276
+ // Check min length
277
+ if (options.minLength !== undefined && input.length < options.minLength) {
278
+ errors.push(`Array must have at least ${options.minLength} items`);
279
+ }
280
+
281
+ // Check max length
282
+ if (options.maxLength !== undefined && input.length > options.maxLength) {
283
+ errors.push(`Array must have at most ${options.maxLength} items`);
284
+ }
285
+
286
+ // Check item types
287
+ if (options.itemType) {
288
+ for (let i = 0; i < input.length; i++) {
289
+ const item = input[i];
290
+ const isValidType =
291
+ options.itemType === "string"
292
+ ? typeof item === "string"
293
+ : options.itemType === "number"
294
+ ? typeof item === "number"
295
+ : typeof item === "object" && item !== null;
296
+
297
+ if (!isValidType) {
298
+ errors.push(`Item at index ${i} is not a ${options.itemType}`);
299
+ }
300
+ }
301
+ }
302
+
303
+ return {
304
+ isValid: errors.length === 0,
305
+ errors,
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Combines multiple validation results
311
+ */
312
+ export function combineValidationResults(
313
+ results: readonly ValidationResult[]
314
+ ): ValidationResult {
315
+ const allErrors = results.flatMap((r) => r.errors);
316
+
317
+ return {
318
+ isValid: allErrors.length === 0,
319
+ errors: allErrors,
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Sanitizes and validates user input in one step
325
+ */
326
+ export function sanitizeAndValidate(
327
+ input: unknown,
328
+ options: StringValidationOptions = {}
329
+ ): { readonly sanitized: string; readonly validation: ValidationResult } {
330
+ const sanitized = sanitizeString(input);
331
+ const validation = validateString(sanitized, options);
332
+
333
+ return { sanitized, validation };
334
+ }
335
+
336
+ /**
337
+ * Validates prompt/input text for AI generation
338
+ */
339
+ export function validateAIPrompt(input: unknown): ValidationResult {
340
+ const options: StringValidationOptions = {
341
+ minLength: MIN_PROMPT_LENGTH,
342
+ maxLength: MAX_PROMPT_LENGTH,
343
+ trim: true,
344
+ };
345
+
346
+ return validateString(input, options);
347
+ }
348
+
349
+ /**
350
+ * Validates image data (base64 or URL)
351
+ */
352
+ export function validateImageData(input: unknown): ValidationResult {
353
+ if (typeof input !== "string") {
354
+ return {
355
+ isValid: false,
356
+ errors: ["Image data must be a string"],
357
+ };
358
+ }
359
+
360
+ // Check if it's a URL
361
+ if (input.startsWith("http://") || input.startsWith("https://")) {
362
+ return validateURL(input);
363
+ }
364
+
365
+ // Check if it's base64
366
+ if (input.startsWith("data:image/")) {
367
+ const base64Part = input.split(",")[1];
368
+ if (!base64Part) {
369
+ return {
370
+ isValid: false,
371
+ errors: ["Invalid data URI format"],
372
+ };
373
+ }
374
+ return validateBase64(base64Part);
375
+ }
376
+
377
+ return {
378
+ isValid: false,
379
+ errors: ["Image data must be a URL or base64 data URI"],
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Validates user ID
385
+ */
386
+ export function validateUserId(input: unknown): ValidationResult {
387
+ const options: StringValidationOptions = {
388
+ minLength: 1,
389
+ maxLength: MAX_USER_ID_LENGTH,
390
+ pattern: /^[a-zA-Z0-9_-]+$/,
391
+ };
392
+
393
+ return validateString(input, options);
394
+ }
395
+
396
+ /**
397
+ * Validates creation ID
398
+ */
399
+ export function validateCreationId(input: unknown): ValidationResult {
400
+ const options: StringValidationOptions = {
401
+ minLength: 1,
402
+ maxLength: MAX_CREATION_ID_LENGTH,
403
+ };
404
+
405
+ return validateString(input, options);
406
+ }
@@ -43,22 +43,11 @@ export const AIGenerationForm: React.FC<AIGenerationFormProps> = ({
43
43
  translations,
44
44
  children,
45
45
  }) => {
46
- if (__DEV__) {
47
- console.log("[AIGenerationForm] RENDERING NOW - hideGenerateButton:", hideGenerateButton);
48
- }
49
-
50
46
  const tokens = useAppDesignTokens();
51
47
  const isAdvancedVisible = showAdvanced !== undefined ? showAdvanced : true;
52
- // Button is disabled if: external isDisabled is true, OR prompt validation fails (when prompt is used)
53
48
  const promptInvalid = onPromptChange ? !prompt?.trim() : false;
54
49
  const buttonIsDisabled = isDisabled || promptInvalid;
55
50
 
56
- useEffect(() => {
57
- if (__DEV__) {
58
- console.log("[AIGenerationForm] MOUNTED/UPDATED - prompt:", prompt, "isGenerating:", isGenerating, "buttonIsDisabled:", buttonIsDisabled, "hideGenerateButton:", hideGenerateButton);
59
- }
60
- }, [prompt, isGenerating, buttonIsDisabled, hideGenerateButton]);
61
-
62
51
  return (
63
52
  <>
64
53
  {presets && presets.length > 0 && onPresetPress && (
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Error Boundary Component
3
+ * Catches JavaScript errors anywhere in the child component tree
4
+ * Displays a fallback UI instead of crashing the entire app
5
+ */
6
+
7
+ import React, { Component, ErrorInfo, ReactNode } from "react";
8
+ import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
9
+
10
+ interface ErrorBoundaryProps {
11
+ readonly children: ReactNode;
12
+ readonly fallback?: ReactNode;
13
+ readonly onError?: (error: Error, errorInfo: ErrorInfo) => void;
14
+ }
15
+
16
+ interface ErrorBoundaryState {
17
+ readonly hasError: boolean;
18
+ readonly error?: Error;
19
+ }
20
+
21
+ export class ErrorBoundary extends Component<
22
+ ErrorBoundaryProps,
23
+ ErrorBoundaryState
24
+ > {
25
+ constructor(props: ErrorBoundaryProps) {
26
+ super(props);
27
+ this.state = { hasError: false };
28
+ }
29
+
30
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
31
+ return { hasError: true, error };
32
+ }
33
+
34
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
35
+ // Log error to error reporting service
36
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
37
+ console.error("[ErrorBoundary] Caught error:", error, errorInfo);
38
+ }
39
+
40
+ this.props.onError?.(error, errorInfo);
41
+ }
42
+
43
+ handleReset = (): void => {
44
+ this.setState({ hasError: false, error: undefined });
45
+ };
46
+
47
+ render(): ReactNode {
48
+ if (this.state.hasError) {
49
+ // Use custom fallback if provided
50
+ if (this.props.fallback) {
51
+ return this.props.fallback;
52
+ }
53
+
54
+ // Default error UI
55
+ return (
56
+ <View style={styles.container}>
57
+ <View style={styles.content}>
58
+ <Text style={styles.title}>Something went wrong</Text>
59
+ <Text style={styles.message}>
60
+ An unexpected error occurred. Please try again.
61
+ </Text>
62
+ {typeof __DEV__ !== "undefined" && __DEV__ && this.state.error && (
63
+ <Text style={styles.errorText}>
64
+ {this.state.error.message}
65
+ </Text>
66
+ )}
67
+ <TouchableOpacity
68
+ style={styles.button}
69
+ onPress={this.handleReset}
70
+ >
71
+ <Text style={styles.buttonText}>Try Again</Text>
72
+ </TouchableOpacity>
73
+ </View>
74
+ </View>
75
+ );
76
+ }
77
+
78
+ return this.props.children;
79
+ }
80
+ }
81
+
82
+ const styles = StyleSheet.create({
83
+ container: {
84
+ flex: 1,
85
+ backgroundColor: "#fff",
86
+ justifyContent: "center",
87
+ alignItems: "center",
88
+ padding: 20,
89
+ },
90
+ content: {
91
+ alignItems: "center",
92
+ maxWidth: 400,
93
+ },
94
+ title: {
95
+ fontSize: 24,
96
+ fontWeight: "bold",
97
+ marginBottom: 12,
98
+ color: "#d32f2f",
99
+ },
100
+ message: {
101
+ fontSize: 16,
102
+ textAlign: "center",
103
+ marginBottom: 20,
104
+ color: "#666",
105
+ lineHeight: 24,
106
+ },
107
+ errorText: {
108
+ fontSize: 12,
109
+ textAlign: "center",
110
+ marginBottom: 20,
111
+ color: "#999",
112
+ fontFamily: "monospace",
113
+ },
114
+ button: {
115
+ backgroundColor: "#2196F3",
116
+ paddingHorizontal: 24,
117
+ paddingVertical: 12,
118
+ borderRadius: 8,
119
+ minWidth: 120,
120
+ alignItems: "center",
121
+ },
122
+ buttonText: {
123
+ color: "#fff",
124
+ fontSize: 16,
125
+ fontWeight: "600",
126
+ },
127
+ });
128
+
129
+ /**
130
+ * Higher-order component version of ErrorBoundary
131
+ */
132
+ export function withErrorBoundary<P extends object>(
133
+ WrappedComponent: React.ComponentType<P>,
134
+ fallback?: ReactNode,
135
+ ): React.ComponentType<P> {
136
+ return (props: P) => (
137
+ <ErrorBoundary fallback={fallback}>
138
+ <WrappedComponent {...props} />
139
+ </ErrorBoundary>
140
+ );
141
+ }
@@ -5,6 +5,7 @@ export { PendingJobProgressBar } from "./PendingJobProgressBar";
5
5
  export { PendingJobCardActions } from "./PendingJobCardActions";
6
6
  export { PromptInput } from "./PromptInput";
7
7
  export { AIGenerationHero } from "./AIGenerationHero";
8
+ export { ErrorBoundary, withErrorBoundary } from "./ErrorBoundary";
8
9
  export * from "./StylePresetsGrid";
9
10
  export * from "./AIGenerationForm";
10
11
  export * from "./AIGenerationForm.types";
@@ -35,7 +35,6 @@ export interface UseBackgroundGenerationReturn<TInput, TResult> {
35
35
  input: TInput,
36
36
  ) => Promise<DirectExecutionResult<TResult>>;
37
37
  readonly cancelJob: (id: string) => void;
38
- readonly retryJob: (id: string) => void;
39
38
  readonly pendingJobs: BackgroundJob<TInput, TResult>[];
40
39
  readonly activeJobCount: number;
41
40
  readonly hasActiveJobs: boolean;
@@ -173,21 +172,10 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
173
172
  [removeJob],
174
173
  );
175
174
 
176
- const retryJob = useCallback(
177
- (id: string) => {
178
- const jobData = jobInputsRef.current.get(id);
179
- if (!jobData) return;
180
- removeJob(id);
181
- void startJob(jobData.input, jobData.type);
182
- },
183
- [removeJob, startJob],
184
- );
185
-
186
175
  return {
187
176
  startJob,
188
177
  executeDirectly,
189
178
  cancelJob,
190
- retryJob,
191
179
  pendingJobs: jobs,
192
180
  activeJobCount: activeJobsRef.current.size,
193
181
  hasActiveJobs: activeJobsRef.current.size > 0,
@@ -53,8 +53,7 @@ export interface AIFeatureCallbacks<TRequest = unknown, TResult = unknown> {
53
53
  calculateCost: (multiplier?: number, _model?: string | null) => number;
54
54
  canAfford: (cost: number) => boolean;
55
55
  isAuthenticated: () => boolean;
56
- /** Called when auth is required. Pass retryCallback to resume after auth. */
57
- onAuthRequired: (retryCallback?: () => void) => void;
56
+ onAuthRequired: () => void;
58
57
  onCreditsRequired: (cost?: number) => void;
59
58
  onSuccess?: (result: TResult) => void;
60
59
  onError?: (error: string) => void;
@@ -98,8 +97,8 @@ export function useAIFeatureCallbacks<TRequest = unknown, TResult = unknown>(
98
97
  [creditCostPerUnit],
99
98
  );
100
99
 
101
- const onAuthRequired = useCallback((retryCallback?: () => void) => {
102
- showAuthModal(retryCallback);
100
+ const onAuthRequired = useCallback(() => {
101
+ showAuthModal();
103
102
  }, [showAuthModal]);
104
103
 
105
104
  const onCreditsRequired = useCallback(
@@ -78,7 +78,6 @@ export interface ResultActionButton {
78
78
  export interface ResultActionsConfig {
79
79
  share?: ResultActionButton;
80
80
  save?: ResultActionButton;
81
- retry?: ResultActionButton;
82
81
  layout?: "horizontal" | "vertical" | "grid";
83
82
  buttonSpacing?: number;
84
83
  spacing?: {
@@ -169,12 +168,6 @@ export const DEFAULT_RESULT_CONFIG: ResultConfig = {
169
168
  variant: "secondary",
170
169
  position: "bottom",
171
170
  },
172
- retry: {
173
- enabled: true,
174
- icon: "refresh",
175
- variant: "text",
176
- position: "top",
177
- },
178
171
  layout: "horizontal",
179
172
  buttonSpacing: 10,
180
173
  spacing: {