@umituz/react-native-ai-generation-content 1.73.0 → 1.74.1

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/domain/constants/queue-status.constants.ts +1 -1
  3. package/src/domains/creations/domain/entities/Creation.ts +2 -2
  4. package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +2 -2
  5. package/src/domains/generation/index.ts +1 -1
  6. package/src/domains/generation/infrastructure/flow/index.ts +1 -1
  7. package/src/domains/generation/infrastructure/flow/useFlow.ts +0 -6
  8. package/src/domains/generation/wizard/domain/types/credit-calculation.types.ts +23 -0
  9. package/src/domains/generation/wizard/index.ts +16 -0
  10. package/src/domains/generation/wizard/infrastructure/utils/creation-persistence.types.ts +3 -0
  11. package/src/domains/generation/wizard/infrastructure/utils/creation-save-operations.ts +3 -0
  12. package/src/domains/generation/wizard/infrastructure/utils/creation-update-operations.ts +1 -1
  13. package/src/domains/generation/wizard/infrastructure/utils/credit-calculator.ts +48 -0
  14. package/src/domains/generation/wizard/infrastructure/utils/credit-value-extractors.ts +44 -0
  15. package/src/domains/generation/wizard/infrastructure/utils/wizard-data-validators.ts +61 -0
  16. package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +5 -0
  17. package/src/domains/generation/wizard/presentation/components/WizardContinueButton.tsx +9 -1
  18. package/src/domains/generation/wizard/presentation/components/WizardFlow.types.ts +3 -0
  19. package/src/domains/generation/wizard/presentation/components/WizardFlowContent.tsx +42 -2
  20. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +3 -2
  21. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.types.ts +2 -0
  22. package/src/domains/generation/wizard/presentation/components/step-renderers/renderPhotoUploadStep.tsx +3 -0
  23. package/src/domains/generation/wizard/presentation/components/step-renderers/renderSelectionStep.tsx +4 -0
  24. package/src/domains/generation/wizard/presentation/components/step-renderers/renderTextInputStep.tsx +3 -3
  25. package/src/domains/generation/wizard/presentation/hooks/use-video-queue-generation.types.ts +1 -0
  26. package/src/domains/generation/wizard/presentation/hooks/useGenerationPhase.ts +1 -1
  27. package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +10 -0
  28. package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +9 -1
  29. package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +2 -0
  30. package/src/domains/generation/wizard/presentation/screens/GenericPhotoUploadScreen.tsx +3 -0
  31. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.tsx +2 -1
  32. package/src/domains/generation/wizard/presentation/screens/SelectionScreen.types.ts +2 -0
  33. package/src/domains/generation/wizard/presentation/screens/TextInputScreen.tsx +21 -2
  34. package/src/domains/image-to-video/presentation/screens/ImageToVideoWizardFlow.tsx +2 -0
  35. package/src/domains/scenarios/README.md +3 -3
  36. package/src/domains/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +2 -0
  37. package/src/domains/text-to-video/presentation/screens/TextToVideoWizardFlow.tsx +2 -0
  38. package/src/index.ts +9 -0
  39. package/src/infrastructure/services/multi-image-generation.executor.ts +1 -3
  40. package/src/infrastructure/utils/error-classification.ts +1 -1
  41. package/src/infrastructure/utils/message-extractor.ts +4 -4
  42. package/src/infrastructure/utils/url-extractor/extraction-rules.ts +6 -6
  43. package/src/infrastructure/utils/url-extractor/image-result-extractor.ts +0 -1
  44. package/src/infrastructure/utils/url-extractor/video-result-extractor.ts +0 -1
  45. package/src/presentation/hooks/generation/useDualImageGeneration.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.73.0",
3
+ "version": "1.74.1",
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",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Queue Status Constants
3
- * FAL queue job status values - shared across all polling hooks
3
+ * Generic queue job status values - shared across all polling hooks
4
4
  */
5
5
 
6
6
  export const QUEUE_STATUS = {
@@ -35,7 +35,7 @@ export interface Creation {
35
35
  // Extended fields for job-based creations
36
36
  readonly status?: CreationStatus;
37
37
  readonly output?: CreationOutput;
38
- // Background job tracking - FAL queue requestId and model
38
+ // Background job tracking
39
39
  readonly requestId?: string;
40
40
  readonly model?: string;
41
41
  // Soft delete - if set, the creation is considered deleted
@@ -61,7 +61,7 @@ export interface CreationDocument {
61
61
  readonly createdAt: FirebaseTimestamp | Date;
62
62
  readonly completedAt?: FirebaseTimestamp | Date | null;
63
63
  readonly deletedAt?: FirebaseTimestamp | Date | null;
64
- // Background job tracking - FAL queue requestId and model
64
+ // Background job tracking
65
65
  readonly requestId?: string;
66
66
  readonly model?: string;
67
67
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * useProcessingJobsPoller Hook
3
- * Polls FAL queue status for "processing" creations and updates Firestore when complete
3
+ * Polls queue status for "processing" creations and updates Firestore when complete
4
4
  * Enables true background generation - works even after wizard is dismissed
5
- * Uses provider registry internally - no need to pass FAL functions
5
+ * Uses provider registry internally - no need to pass provider functions
6
6
  */
7
7
 
8
8
  import { useEffect, useRef, useMemo } from "react";
@@ -78,7 +78,7 @@ export {
78
78
  } from "./wizard";
79
79
 
80
80
  // Flow Infrastructure
81
- export { createFlowStore, useFlow, resetFlowStore } from "./infrastructure/flow";
81
+ export { createFlowStore, useFlow } from "./infrastructure/flow";
82
82
  export type { FlowStoreType } from "./infrastructure/flow";
83
83
 
84
84
  // Flow config types from domain
@@ -4,4 +4,4 @@
4
4
  */
5
5
 
6
6
  export { createFlowStore, type FlowStoreType } from "./useFlowStore";
7
- export { useFlow, resetFlowStore } from "./useFlow";
7
+ export { useFlow } from "./useFlow";
@@ -105,9 +105,3 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
105
105
  };
106
106
 
107
107
  declare const __DEV__: boolean;
108
-
109
- export const resetFlowStore = () => {
110
- if (typeof __DEV__ !== "undefined" && __DEV__) {
111
- console.warn('resetFlowStore is deprecated. Each component now maintains its own flow store instance.');
112
- }
113
- };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Credit Calculation Types
3
+ * Domain types for credit calculation system
4
+ * Single Source of Truth for credit-related type definitions
5
+ */
6
+
7
+ /**
8
+ * Credit Calculator Function Type
9
+ * Apps provide implementation, package calls it
10
+ *
11
+ * @param params - Calculation parameters
12
+ * @param params.duration - Video duration in seconds
13
+ * @param params.resolution - Video resolution ("480p" | "720p")
14
+ * @param params.outputType - Output type ("video" | "image")
15
+ * @param params.hasImageInput - Whether input includes image (image-to-video)
16
+ * @returns Calculated credit cost
17
+ */
18
+ export type CreditCalculatorFn = (params: {
19
+ duration?: number;
20
+ resolution?: string;
21
+ outputType: "video" | "image";
22
+ hasImageInput: boolean;
23
+ }) => number;
@@ -32,6 +32,22 @@ export {
32
32
  quickBuildWizard,
33
33
  } from "./infrastructure/builders/dynamic-step-builder";
34
34
 
35
+ // Infrastructure - Generic Credit Calculator (USD to Credits conversion ONLY)
36
+ export {
37
+ convertCostToCredits,
38
+ getCreditConfig,
39
+ } from "./infrastructure/utils/credit-calculator";
40
+
41
+ // Infrastructure - Data Validators
42
+ export {
43
+ validateDuration,
44
+ validateResolution,
45
+ } from "./infrastructure/utils/wizard-data-validators";
46
+ export type { ValidationResult } from "./infrastructure/utils/wizard-data-validators";
47
+
48
+ // Credit Calculator Function Type (for apps to implement)
49
+ export type { CreditCalculatorFn } from "./domain/types/credit-calculation.types";
50
+
35
51
  // Presentation - Hooks
36
52
  export { usePhotoUploadState } from "./presentation/hooks/usePhotoUploadState";
37
53
  export type {
@@ -13,6 +13,9 @@ export interface ProcessingCreationData {
13
13
  readonly prompt: string;
14
14
  readonly requestId?: string;
15
15
  readonly model?: string;
16
+ readonly duration?: number;
17
+ readonly resolution?: string;
18
+ readonly creditCost?: number;
16
19
  }
17
20
 
18
21
  export interface CompletedCreationData {
@@ -33,6 +33,9 @@ export async function saveAsProcessing(
33
33
  metadata: {
34
34
  scenarioId: data.scenarioId,
35
35
  scenarioTitle: data.scenarioTitle,
36
+ ...(data.duration && { duration: data.duration }),
37
+ ...(data.resolution && { resolution: data.resolution }),
38
+ ...(data.creditCost && { creditCost: data.creditCost }),
36
39
  },
37
40
  });
38
41
 
@@ -52,7 +52,7 @@ export async function updateToFailed(
52
52
  }
53
53
 
54
54
  /**
55
- * Update creation with FAL queue requestId and model after job submission
55
+ * Update creation with queue requestId after job submission
56
56
  */
57
57
  export async function updateRequestId(
58
58
  repository: ICreationsRepository,
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Generic Credit Calculator
3
+ * Package is GENERIC - knows nothing about specific pricing
4
+ * All costs are determined by the APP
5
+ *
6
+ * This package ONLY converts USD cost to credits using app-provided formula
7
+ */
8
+
9
+ /**
10
+ * Credit pricing configuration
11
+ * These are the ONLY constants the package knows about
12
+ */
13
+ const CREDIT_CONFIG = {
14
+ CUSTOMER_PRICE_PER_CREDIT: 0.10,
15
+ MARKUP_MULTIPLIER: 1.67,
16
+ } as const;
17
+
18
+ /**
19
+ * Convert USD cost to credits
20
+ * This is the ONLY calculation the package does
21
+ * Formula: credits = ceil((cost * markup) / credit_price)
22
+ *
23
+ * @param costInUSD - The cost in USD (provided by app)
24
+ * @throws {Error} If cost is invalid
25
+ */
26
+ export function convertCostToCredits(costInUSD: number): number {
27
+ if (!costInUSD || typeof costInUSD !== "number" || costInUSD < 0) {
28
+ throw new Error(
29
+ `[convertCostToCredits] Invalid cost: ${costInUSD}. Must be a non-negative number.`
30
+ );
31
+ }
32
+
33
+ const { CUSTOMER_PRICE_PER_CREDIT, MARKUP_MULTIPLIER } = CREDIT_CONFIG;
34
+ const credits = (costInUSD * MARKUP_MULTIPLIER) / CUSTOMER_PRICE_PER_CREDIT;
35
+
36
+ return Math.ceil(credits);
37
+ }
38
+
39
+ /**
40
+ * Get credit configuration
41
+ * Exposes config for app to use if needed
42
+ */
43
+ export function getCreditConfig() {
44
+ return {
45
+ customerPricePerCredit: CREDIT_CONFIG.CUSTOMER_PRICE_PER_CREDIT,
46
+ markupMultiplier: CREDIT_CONFIG.MARKUP_MULTIPLIER,
47
+ };
48
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Credit Value Extractors
3
+ * Pure utility functions to extract and normalize values from customData
4
+ * Single Responsibility: Data transformation for credit calculation
5
+ */
6
+
7
+ /**
8
+ * Extract duration value from customData
9
+ * Handles both number and string formats ("4s", "5s", "6s")
10
+ *
11
+ * @param value - Raw value from customData
12
+ * @returns Normalized duration number, or undefined if invalid
13
+ */
14
+ export function extractDuration(value: unknown): number | undefined {
15
+ // Already a number
16
+ if (typeof value === "number" && value > 0) {
17
+ return value;
18
+ }
19
+
20
+ // String format: "4s", "5s", "6s" → parse to number
21
+ if (typeof value === "string") {
22
+ const match = value.match(/^(\d+)s?$/);
23
+ if (match) {
24
+ const parsed = parseInt(match[1], 10);
25
+ return parsed > 0 ? parsed : undefined;
26
+ }
27
+ }
28
+
29
+ return undefined;
30
+ }
31
+
32
+ /**
33
+ * Extract resolution value from customData
34
+ * Validates against allowed values
35
+ *
36
+ * @param value - Raw value from customData
37
+ * @returns Normalized resolution string, or undefined if invalid
38
+ */
39
+ export function extractResolution(value: unknown): "480p" | "720p" | undefined {
40
+ if (value === "480p" || value === "720p") {
41
+ return value;
42
+ }
43
+ return undefined;
44
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Wizard Data Validators
3
+ * Centralized validation utilities for wizard generation data
4
+ * DRY: Used across AIGenerateScreen, TextToVideoWizardScreen, ImageToVideoWizardScreen
5
+ */
6
+
7
+ export interface ValidationResult<T> {
8
+ value?: T;
9
+ error?: string;
10
+ }
11
+
12
+ /**
13
+ * Validate and extract duration from wizard data
14
+ *
15
+ * @param data - Wizard generation data
16
+ * @returns Validation result with value or error
17
+ */
18
+ export function validateDuration(
19
+ data: Record<string, unknown>,
20
+ ): ValidationResult<number> {
21
+ const duration = data.duration as number;
22
+
23
+ if (!duration || typeof duration !== "number" || duration <= 0) {
24
+ return {
25
+ error: `Invalid duration: ${duration}. Must be a positive number.`,
26
+ };
27
+ }
28
+
29
+ return { value: duration };
30
+ }
31
+
32
+ /**
33
+ * Validate and extract resolution from wizard data
34
+ *
35
+ * @param data - Wizard generation data
36
+ * @returns Validation result with resolution or error
37
+ */
38
+ export function validateResolution(
39
+ data: Record<string, unknown>,
40
+ ): ValidationResult<"480p" | "720p"> {
41
+ const resolutionValue = data.resolution as string;
42
+
43
+ if (!resolutionValue || typeof resolutionValue !== "string") {
44
+ return {
45
+ error: `Invalid resolution: ${resolutionValue}. Must be a string.`,
46
+ };
47
+ }
48
+
49
+ // Map resolution - EXACT MATCH ONLY
50
+ if (resolutionValue === "480p") {
51
+ return { value: "480p" };
52
+ }
53
+
54
+ if (resolutionValue === "720p") {
55
+ return { value: "720p" };
56
+ }
57
+
58
+ return {
59
+ error: `Invalid resolution value: "${resolutionValue}". Must be "480p" or "720p".`,
60
+ };
61
+ }
@@ -10,6 +10,7 @@ import type { WizardFeatureConfig } from "../../domain/entities/wizard-feature.t
10
10
  import type { WizardScenarioData } from "../hooks/useWizardGeneration";
11
11
  import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
12
12
  import type { GenerationErrorInfo } from "./WizardFlow.types";
13
+ import type { CreditCalculatorFn } from "../../domain/types/credit-calculation.types";
13
14
  import { validateScenario } from "../utilities/validateScenario";
14
15
  import { WizardFlowContent } from "./WizardFlowContent";
15
16
  import {
@@ -27,6 +28,8 @@ export interface GenericWizardFlowProps {
27
28
  readonly alertMessages: AlertMessages;
28
29
  /** Credit cost for this generation - REQUIRED, determined by the app */
29
30
  readonly creditCost: number;
31
+ /** Calculator function provided by APP - package calls this to get dynamic cost */
32
+ readonly calculateCredits?: CreditCalculatorFn;
30
33
  readonly skipResultStep?: boolean;
31
34
  readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
32
35
  readonly onGenerationStart?: (
@@ -53,6 +56,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
53
56
  userId,
54
57
  alertMessages,
55
58
  creditCost,
59
+ calculateCredits,
56
60
  skipResultStep = false,
57
61
  onStepChange,
58
62
  onGenerationStart,
@@ -109,6 +113,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = (props) => {
109
113
  userId={userId}
110
114
  alertMessages={alertMessages}
111
115
  creditCost={creditCost}
116
+ calculateCredits={calculateCredits}
112
117
  skipResultStep={skipResultStep}
113
118
  onStepChange={onStepChange}
114
119
  onGenerationStart={onGenerationStart}
@@ -19,6 +19,7 @@ export interface WizardContinueButtonProps {
19
19
  readonly onPress: () => void;
20
20
  readonly label: string;
21
21
  readonly icon?: IconName;
22
+ readonly creditCost?: number;
22
23
  }
23
24
 
24
25
  export function WizardContinueButton({
@@ -26,6 +27,7 @@ export function WizardContinueButton({
26
27
  onPress,
27
28
  label,
28
29
  icon = "chevron-forward-outline",
30
+ creditCost,
29
31
  }: WizardContinueButtonProps) {
30
32
  const tokens = useAppDesignTokens();
31
33
  const { isTabletDevice, minTouchTarget } = useResponsive();
@@ -34,6 +36,12 @@ export function WizardContinueButton({
34
36
  const buttonMinHeight = Math.max(minTouchTarget, 44);
35
37
  const buttonMinWidth = isTabletDevice ? 120 : 100;
36
38
 
39
+ // If creditCost is provided, append it to the label
40
+ const displayLabel =
41
+ creditCost !== undefined && creditCost > 0
42
+ ? `${label} (${creditCost} credits)`
43
+ : label;
44
+
37
45
  return (
38
46
  <TouchableOpacity
39
47
  onPress={onPress}
@@ -60,7 +68,7 @@ export function WizardContinueButton({
60
68
  { color: canContinue ? tokens.colors.onPrimary : tokens.colors.textSecondary },
61
69
  ]}
62
70
  >
63
- {label}
71
+ {displayLabel}
64
72
  </AtomicText>
65
73
  <AtomicIcon
66
74
  name={icon}
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
6
+ import type { CreditCalculatorFn } from "../../domain/types/credit-calculation.types";
6
7
 
7
8
  /**
8
9
  * Error information with refund eligibility
@@ -23,6 +24,8 @@ export interface BaseWizardFlowProps {
23
24
  readonly userId?: string;
24
25
  /** Credit cost for this generation - REQUIRED, determined by the app */
25
26
  readonly creditCost: number;
27
+ /** Calculator function provided by APP - package calls this to get dynamic cost */
28
+ readonly calculateCredits?: CreditCalculatorFn;
26
29
  /** Called when network is unavailable and generation is blocked */
27
30
  readonly onNetworkError?: () => void;
28
31
  /** Called when generation completes */
@@ -16,8 +16,10 @@ import type { Creation } from "../../../../creations/domain/entities/Creation";
16
16
  import { createCreationsRepository } from "../../../../creations";
17
17
  import { useResultActions } from "../../../../result-preview/presentation/hooks/useResultActions";
18
18
  import { useWizardFlowHandlers } from "../hooks/useWizardFlowHandlers";
19
+ import { extractDuration, extractResolution } from "../../infrastructure/utils/credit-value-extractors";
19
20
  import { WizardStepRenderer } from "./WizardStepRenderer";
20
21
  import { StarRatingPicker } from "../../../../result-preview/presentation/components/StarRatingPicker";
22
+ import type { CreditCalculatorFn } from "../../domain/types/credit-calculation.types";
21
23
 
22
24
  export interface WizardFlowContentProps {
23
25
  readonly featureConfig: WizardFeatureConfig;
@@ -27,6 +29,8 @@ export interface WizardFlowContentProps {
27
29
  readonly alertMessages: AlertMessages;
28
30
  /** Credit cost for this generation - REQUIRED, determined by the app */
29
31
  readonly creditCost: number;
32
+ /** Calculator function provided by APP - package calls this to get dynamic cost */
33
+ readonly calculateCredits?: CreditCalculatorFn;
30
34
  readonly skipResultStep?: boolean;
31
35
  readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
32
36
  readonly onGenerationStart?: (
@@ -51,7 +55,8 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
51
55
  validatedScenario,
52
56
  userId,
53
57
  alertMessages,
54
- creditCost,
58
+ creditCost, // Still needed for initial feature gate in parent
59
+ calculateCredits, // Calculator function from APP
55
60
  skipResultStep = false,
56
61
  onStepChange,
57
62
  onGenerationStart,
@@ -103,6 +108,40 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
103
108
  videoUrl: resultVideoUrl,
104
109
  });
105
110
 
111
+ /**
112
+ * Calculate credit cost - CENTRALIZED CALCULATION
113
+ * React Best Practice: Calculate derived state during render (not in useEffect)
114
+ *
115
+ * Flow:
116
+ * 1. Extract values from customData using utility functions
117
+ * 2. Call app's calculator function with normalized values
118
+ * 3. Fallback to static creditCost if calculation incomplete
119
+ */
120
+ const calculatedCreditCost = useMemo(() => {
121
+ // If no calculator provided, use static creditCost
122
+ if (!calculateCredits) {
123
+ return creditCost;
124
+ }
125
+
126
+ const outputType = validatedScenario.outputType as "video" | "image";
127
+ const hasImageInput = validatedScenario.inputType !== "text";
128
+
129
+ // Extract and normalize values from customData
130
+ const duration = extractDuration(customData.duration);
131
+ const resolution = extractResolution(customData.resolution);
132
+
133
+ // Call app's calculator
134
+ const result = calculateCredits({
135
+ duration,
136
+ resolution,
137
+ outputType,
138
+ hasImageInput,
139
+ });
140
+
141
+ // If result is 0 (incomplete selections), use static initial cost
142
+ return result > 0 ? result : creditCost;
143
+ }, [customData, validatedScenario.outputType, validatedScenario.inputType, calculateCredits, creditCost]);
144
+
106
145
  const handlers = useWizardFlowHandlers({
107
146
  currentStepIndex,
108
147
  flowSteps,
@@ -131,7 +170,7 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
131
170
  userId,
132
171
  isGeneratingStep: currentStep?.type === StepType.GENERATING,
133
172
  alertMessages,
134
- creditCost,
173
+ creditCost: calculatedCreditCost,
135
174
  onSuccess: handlers.handleGenerationComplete,
136
175
  onError: handlers.handleGenerationError,
137
176
  onCreditsExhausted,
@@ -155,6 +194,7 @@ export const WizardFlowContent: React.FC<WizardFlowContentProps> = (props) => {
155
194
  isSaving={isSaving}
156
195
  isSharing={isSharing}
157
196
  showRating={Boolean(userId) && !hasRated}
197
+ creditCost={calculatedCreditCost}
158
198
  onNext={handlers.handleNextStep}
159
199
  onBack={handlers.handleBack}
160
200
  onPhotoContinue={handlers.handlePhotoContinue}
@@ -25,6 +25,7 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
25
25
  isSaving,
26
26
  isSharing,
27
27
  showRating = true,
28
+ creditCost,
28
29
  onNext,
29
30
  onBack,
30
31
  onPhotoContinue,
@@ -89,13 +90,13 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
89
90
  }
90
91
 
91
92
  case StepType.PARTNER_UPLOAD:
92
- return renderPhotoUploadStep({ key: step.id, step, customData, onBack, onPhotoContinue, t });
93
+ return renderPhotoUploadStep({ key: step.id, step, customData, onBack, onPhotoContinue, t, creditCost });
93
94
 
94
95
  case StepType.TEXT_INPUT:
95
96
  return renderTextInputStep({ key: step.id, step, customData, onBack, onPhotoContinue, t, alertMessages });
96
97
 
97
98
  case StepType.FEATURE_SELECTION:
98
- return renderSelectionStep({ key: step.id, step, customData, onBack, onPhotoContinue, t });
99
+ return renderSelectionStep({ key: step.id, step, customData, onBack, onPhotoContinue, t, creditCost });
99
100
 
100
101
  default:
101
102
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -12,6 +12,8 @@ export interface WizardStepRendererProps {
12
12
  readonly isSaving: boolean;
13
13
  readonly isSharing: boolean;
14
14
  readonly showRating?: boolean;
15
+ /** Calculated credit cost - passed from parent */
16
+ readonly creditCost?: number;
15
17
  readonly onNext: () => void;
16
18
  readonly onBack: () => void;
17
19
  readonly onPhotoContinue: (stepId: string, image: UploadedImage) => void;
@@ -15,6 +15,7 @@ export interface PhotoUploadStepProps {
15
15
  readonly onBack: () => void;
16
16
  readonly onPhotoContinue: (stepId: string, image: UploadedImage) => void;
17
17
  readonly t: (key: string) => string;
18
+ readonly creditCost?: number;
18
19
  }
19
20
 
20
21
  export function renderPhotoUploadStep({
@@ -23,6 +24,7 @@ export function renderPhotoUploadStep({
23
24
  onBack,
24
25
  onPhotoContinue,
25
26
  t,
27
+ creditCost,
26
28
  }: PhotoUploadStepProps): React.ReactElement {
27
29
  const wizardConfig = getWizardStepConfig(step.config);
28
30
  const titleKey = wizardConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
@@ -46,6 +48,7 @@ export function renderPhotoUploadStep({
46
48
  uploadFailed: t("common.errors.upload_failed"),
47
49
  }}
48
50
  t={t}
51
+ creditCost={creditCost}
49
52
  onBack={onBack}
50
53
  onContinue={(image) => onPhotoContinue(step.id, image)}
51
54
  existingImage={existingPhoto}
@@ -15,6 +15,8 @@ export interface SelectionStepProps {
15
15
  readonly onBack: () => void;
16
16
  readonly onPhotoContinue: (stepId: string, image: UploadedImage) => void;
17
17
  readonly t: (key: string) => string;
18
+ /** Calculated credit cost from parent */
19
+ readonly creditCost?: number;
18
20
  }
19
21
 
20
22
  declare const __DEV__: boolean;
@@ -25,6 +27,7 @@ export function renderSelectionStep({
25
27
  onBack,
26
28
  onPhotoContinue,
27
29
  t,
30
+ creditCost,
28
31
  }: SelectionStepProps): React.ReactElement {
29
32
  const selectionConfig = getSelectionConfig(step.config);
30
33
  const titleKey = selectionConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
@@ -76,6 +79,7 @@ export function renderSelectionStep({
76
79
  layout: selectionConfig?.layout,
77
80
  }}
78
81
  initialValue={initialValue}
82
+ creditCost={creditCost}
79
83
  onBack={onBack}
80
84
  onContinue={(value) => {
81
85
  onPhotoContinue(step.id, { uri: String(value), selection: value, previewUrl: "" } as UploadedImage);
@@ -48,9 +48,9 @@ export function renderTextInputStep({
48
48
  contentNotAllowedMessage: alertMessages?.policyViolation || "This type of content is not supported. Please try a different prompt.",
49
49
  }}
50
50
  config={{
51
- minLength: textConfig?.minLength ?? 3,
52
- maxLength: textConfig?.maxLength ?? 1000,
53
- multiline: textConfig?.multiline ?? true,
51
+ minLength: textConfig?.minLength !== undefined ? textConfig.minLength : 3,
52
+ maxLength: textConfig?.maxLength !== undefined ? textConfig.maxLength : 1000,
53
+ multiline: textConfig?.multiline !== undefined ? textConfig.multiline : true,
54
54
  }}
55
55
  initialValue={existingText}
56
56
  onBack={onBack}
@@ -11,6 +11,7 @@ export interface UseVideoQueueGenerationProps {
11
11
  readonly scenario: WizardScenarioData;
12
12
  readonly persistence: CreationPersistence;
13
13
  readonly strategy: WizardStrategy;
14
+ readonly creditCost?: number;
14
15
  readonly onSuccess?: (result: unknown) => void;
15
16
  readonly onError?: (error: string) => void;
16
17
  }
@@ -2,7 +2,7 @@
2
2
  * useGenerationPhase Hook
3
3
  * Derives generation phase from elapsed time for UX feedback
4
4
  *
5
- * Best Practice: Since actual API progress is unknown (FAL only returns IN_QUEUE/IN_PROGRESS),
5
+ * Best Practice: Since actual API progress is unknown (queue systems only return IN_QUEUE/IN_PROGRESS),
6
6
  * we use time-based phases to provide meaningful feedback to users.
7
7
  */
8
8
 
@@ -20,6 +20,7 @@ export interface UsePhotoBlockingGenerationProps {
20
20
  readonly scenario: WizardScenarioData;
21
21
  readonly persistence: CreationPersistence;
22
22
  readonly strategy: WizardStrategy;
23
+ readonly creditCost?: number;
23
24
  readonly alertMessages: AlertMessages;
24
25
  readonly onSuccess?: (result: unknown) => void;
25
26
  readonly onError?: (error: string) => void;
@@ -38,6 +39,7 @@ export function usePhotoBlockingGeneration(
38
39
  userId,
39
40
  scenario,
40
41
  persistence,
42
+ creditCost,
41
43
  strategy,
42
44
  alertMessages,
43
45
  onSuccess,
@@ -105,10 +107,18 @@ export function usePhotoBlockingGeneration(
105
107
  // Save to Firestore first
106
108
  if (userId && prompt) {
107
109
  try {
110
+ // Extract generation parameters from input (for image generation, no duration/resolution)
111
+ const inputData = input as Record<string, unknown>;
112
+ const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
113
+ const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
114
+
108
115
  const creationId = await persistence.saveAsProcessing(userId, {
109
116
  scenarioId: scenario.id,
110
117
  scenarioTitle: scenario.title || scenario.id,
111
118
  prompt,
119
+ duration,
120
+ resolution,
121
+ creditCost,
112
122
  });
113
123
  creationIdRef.current = creationId;
114
124
 
@@ -14,7 +14,7 @@ import type {
14
14
  } from "./use-video-queue-generation.types";
15
15
 
16
16
  export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): UseVideoQueueGenerationReturn {
17
- const { userId, scenario, persistence, strategy, onSuccess, onError } = props;
17
+ const { userId, scenario, persistence, strategy, creditCost, onSuccess, onError } = props;
18
18
 
19
19
  const creationIdRef = useRef<string | null>(null);
20
20
  const requestIdRef = useRef<string | null>(null);
@@ -153,10 +153,18 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
153
153
  let creationId: string | null = null;
154
154
  if (userId && prompt) {
155
155
  try {
156
+ // Extract generation parameters from input
157
+ const inputData = input as Record<string, unknown>;
158
+ const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
159
+ const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
160
+
156
161
  creationId = await persistence.saveAsProcessing(userId, {
157
162
  scenarioId: scenario.id,
158
163
  scenarioTitle: scenario.title || scenario.id,
159
164
  prompt,
165
+ duration,
166
+ resolution,
167
+ creditCost,
160
168
  });
161
169
  creationIdRef.current = creationId;
162
170
  } catch (error) {
@@ -51,6 +51,7 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
51
51
  scenario,
52
52
  persistence,
53
53
  strategy,
54
+ creditCost,
54
55
  onSuccess,
55
56
  onError,
56
57
  });
@@ -60,6 +61,7 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
60
61
  scenario,
61
62
  persistence,
62
63
  strategy,
64
+ creditCost,
63
65
  alertMessages,
64
66
  onSuccess,
65
67
  onError,
@@ -46,6 +46,7 @@ export interface PhotoUploadScreenProps {
46
46
  readonly translations: PhotoUploadScreenTranslations;
47
47
  readonly t: (key: string) => string;
48
48
  readonly config?: PhotoUploadScreenConfig;
49
+ readonly creditCost?: number;
49
50
  readonly onBack: () => void;
50
51
  readonly onContinue: (image: UploadedImage) => void;
51
52
  readonly existingImage?: UploadedImage | null;
@@ -58,6 +59,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
58
59
  translations,
59
60
  t,
60
61
  config = DEFAULT_CONFIG,
62
+ creditCost,
61
63
  onBack,
62
64
  onContinue,
63
65
  existingImage,
@@ -114,6 +116,7 @@ export const GenericPhotoUploadScreen: React.FC<PhotoUploadScreenProps> = ({
114
116
  canContinue={canContinue && !!image}
115
117
  onPress={handleContinuePress}
116
118
  label={translations.continue}
119
+ creditCost={creditCost}
117
120
  />
118
121
  }
119
122
  />
@@ -31,6 +31,7 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
31
31
  options,
32
32
  config,
33
33
  initialValue,
34
+ creditCost,
34
35
  onBack,
35
36
  onContinue,
36
37
  }) => {
@@ -128,7 +129,7 @@ export const SelectionScreen: React.FC<SelectionScreenProps> = ({
128
129
  title=""
129
130
  onBackPress={onBack}
130
131
  rightElement={
131
- <WizardContinueButton canContinue={canContinue} onPress={handleContinue} label={translations.continueButton} icon="arrow-forward" />
132
+ <WizardContinueButton canContinue={canContinue} onPress={handleContinue} label={translations.continueButton} icon="arrow-forward" creditCost={creditCost} />
132
133
  }
133
134
  />
134
135
  <ScreenLayout scrollable={true} edges={["left", "right"]} hideScrollIndicator={true} contentContainerStyle={styles.scrollContent}>
@@ -28,6 +28,8 @@ export interface SelectionScreenProps {
28
28
  readonly options: readonly SelectionOption[];
29
29
  readonly config?: SelectionScreenConfig;
30
30
  readonly initialValue?: string | string[];
31
+ /** Calculated credit cost - passed from parent */
32
+ readonly creditCost?: number;
31
33
  readonly onBack: () => void;
32
34
  readonly onContinue: (selectedValue: string | string[]) => void;
33
35
  }
@@ -40,8 +40,27 @@ export const TextInputScreen: React.FC<TextInputScreenProps> = ({
40
40
  const alert = useAlert();
41
41
  const [text, setText] = useState(initialValue);
42
42
 
43
- const minLength = config?.minLength ?? 3;
44
- const maxLength = config?.maxLength ?? 1000;
43
+ // Validate config - REQUIRED, NO DEFAULTS
44
+ if (!config) {
45
+ throw new Error("[TextInputScreen] Config is required but was not provided.");
46
+ }
47
+ if (typeof config.minLength !== "number" || config.minLength < 0) {
48
+ throw new Error(
49
+ `[TextInputScreen] Invalid minLength: ${config.minLength}. Must be a non-negative number.`
50
+ );
51
+ }
52
+ if (typeof config.maxLength !== "number" || config.maxLength <= 0) {
53
+ throw new Error(
54
+ `[TextInputScreen] Invalid maxLength: ${config.maxLength}. Must be a positive number.`
55
+ );
56
+ }
57
+ if (config.minLength > config.maxLength) {
58
+ throw new Error(
59
+ `[TextInputScreen] Invalid config: minLength (${config.minLength}) > maxLength (${config.maxLength}).`
60
+ );
61
+ }
62
+
63
+ const { minLength, maxLength } = config;
45
64
  const canContinue = text.trim().length >= minLength;
46
65
 
47
66
  const handleContinue = useCallback(async () => {
@@ -24,6 +24,7 @@ export const ImageToVideoWizardFlow: React.FC<ImageToVideoWizardFlowProps> = (pr
24
24
  model,
25
25
  userId,
26
26
  creditCost,
27
+ calculateCredits,
27
28
  onNetworkError,
28
29
  onGenerationComplete,
29
30
  onGenerationError,
@@ -70,6 +71,7 @@ export const ImageToVideoWizardFlow: React.FC<ImageToVideoWizardFlowProps> = (pr
70
71
  userId={userId}
71
72
  alertMessages={alertMessages ?? defaultAlerts}
72
73
  creditCost={creditCost}
74
+ calculateCredits={calculateCredits}
73
75
  skipResultStep={true}
74
76
  onGenerationStart={handleGenerationStart}
75
77
  onGenerationComplete={handleGenerationComplete}
@@ -38,7 +38,7 @@ export const APP_SCENARIOS: ScenarioData[] = [
38
38
  prompt: 'A powerful warrior in fantasy armor, epic lighting...',
39
39
  inputType: 'single',
40
40
  outputType: 'image',
41
- model: 'fal-ai/flux/dev',
41
+ model: 'your-provider/text-to-image',
42
42
  },
43
43
  // ... more scenarios
44
44
  ];
@@ -121,7 +121,7 @@ const VIDEO_SCENARIOS: ScenarioData[] = [
121
121
  category: ScenarioCategory.SOLO_CINEMATIC,
122
122
  inputType: 'single',
123
123
  outputType: 'video',
124
- model: 'fal-ai/kling-video/v1.5/pro/image-to-video',
124
+ model: 'your-provider/image-to-video',
125
125
  // ... data
126
126
  }
127
127
  ];
@@ -135,7 +135,7 @@ const SOLO_SCENARIOS: ScenarioData[] = [
135
135
  category: ScenarioCategory.SOLO_FANTASY,
136
136
  inputType: 'single',
137
137
  outputType: 'image',
138
- model: 'fal-ai/flux/dev',
138
+ model: 'your-provider/text-to-image',
139
139
  // ... data
140
140
  }
141
141
  ];
@@ -23,6 +23,7 @@ export const TextToImageWizardFlow: React.FC<TextToImageWizardFlowProps> = (prop
23
23
  model,
24
24
  userId,
25
25
  creditCost,
26
+ calculateCredits,
26
27
  onNetworkError,
27
28
  onGenerationComplete,
28
29
  onGenerationError,
@@ -71,6 +72,7 @@ export const TextToImageWizardFlow: React.FC<TextToImageWizardFlowProps> = (prop
71
72
  userId={userId}
72
73
  alertMessages={alertMessages}
73
74
  creditCost={creditCost}
75
+ calculateCredits={calculateCredits}
74
76
  skipResultStep={true}
75
77
  onGenerationStart={handleGenerationStart}
76
78
  onGenerationComplete={handleGenerationComplete}
@@ -24,6 +24,7 @@ export const TextToVideoWizardFlow: React.FC<TextToVideoWizardFlowProps> = (prop
24
24
  model,
25
25
  userId,
26
26
  creditCost,
27
+ calculateCredits,
27
28
  onNetworkError,
28
29
  onGenerationComplete,
29
30
  onGenerationError,
@@ -70,6 +71,7 @@ export const TextToVideoWizardFlow: React.FC<TextToVideoWizardFlowProps> = (prop
70
71
  userId={userId}
71
72
  alertMessages={alertMessages ?? defaultAlerts}
72
73
  creditCost={creditCost}
74
+ calculateCredits={calculateCredits}
73
75
  skipResultStep={true}
74
76
  onGenerationStart={handleGenerationStart}
75
77
  onGenerationComplete={handleGenerationComplete}
package/src/index.ts CHANGED
@@ -31,3 +31,12 @@ export {
31
31
  IMAGE_TO_VIDEO_WIZARD_CONFIG,
32
32
  } from "./domains/generation/wizard";
33
33
  export type { WizardScenarioData } from "./domains/generation/wizard";
34
+
35
+ // Wizard Validators and Credit Utilities
36
+ export {
37
+ validateDuration,
38
+ validateResolution,
39
+ convertCostToCredits,
40
+ getCreditConfig,
41
+ } from "./domains/generation/wizard";
42
+ export type { ValidationResult, CreditCalculatorFn } from "./domains/generation/wizard";
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Multi-Image Generation Executor
3
- * Handles image generation with multiple input images (e.g., baby prediction)
4
- * Sends image_urls array as required by FAL AI nano-banana/edit model
3
+ * Handles image generation with multiple input images
5
4
  */
6
5
 
7
6
  import { validateProvider } from "../utils/provider-validator.util";
@@ -43,7 +42,6 @@ export interface MultiImageGenerationResult {
43
42
 
44
43
  /**
45
44
  * Execute image generation with multiple input images
46
- * Sends image_urls array as required by FAL AI API
47
45
  */
48
46
  export async function executeMultiImageGeneration(
49
47
  input: MultiImageGenerationInput,
@@ -50,7 +50,7 @@ export function classifyError(error: unknown): AIErrorInfo {
50
50
  });
51
51
  }
52
52
 
53
- // 422 = Content Policy Violation (Fal AI and other providers)
53
+ // 422 = Content Policy Violation
54
54
  if (statusCode === 422 || matchesPatterns(message, CONTENT_POLICY_PATTERNS)) {
55
55
  return logClassification({
56
56
  type: AIErrorType.CONTENT_POLICY,
@@ -38,8 +38,8 @@ export function getErrorTranslationKey(error: unknown): string | null {
38
38
  }
39
39
 
40
40
  /**
41
- * Extract error message from FAL API and other error formats
42
- * Supports: Error instances, FAL API errors, generic objects
41
+ * Extract error message from API responses
42
+ * Supports: Error instances, API errors, generic objects
43
43
  * Returns translation key if available, otherwise original message
44
44
  */
45
45
  export function extractErrorMessage(
@@ -63,10 +63,10 @@ export function extractErrorMessage(
63
63
  } else if (typeof error === "object" && error !== null) {
64
64
  const errObj = error as Record<string, unknown>;
65
65
 
66
- // FAL API error format: {detail: [{msg, type, loc}]}
66
+ // API error format: {detail: [{msg, type, loc}]}
67
67
  if (Array.isArray(errObj.detail) && errObj.detail[0]?.msg) {
68
68
  const detailType = errObj.detail[0]?.type;
69
- // Check for content policy in FAL API response
69
+ // Check for content policy in API response
70
70
  if (detailType === "content_policy_violation") {
71
71
  return `error.generation.${GenerationErrorType.CONTENT_POLICY}`;
72
72
  }
@@ -16,15 +16,15 @@ export interface ExtractionRule {
16
16
 
17
17
  /**
18
18
  * Image extraction rules - checked in order, first success wins
19
- * Supports: FAL.ai wrapper, birefnet, rembg, flux, and direct formats
19
+ * Supports various response wrapper formats and direct formats
20
20
  */
21
21
  export const IMAGE_EXTRACTION_RULES: readonly ExtractionRule[] = [
22
- // FAL.ai data wrapper formats
22
+ // Data wrapper formats
23
23
  { path: ["data", "image"], description: "data.image (string)" },
24
24
  { path: ["data", "imageUrl"], description: "data.imageUrl" },
25
25
  { path: ["data", "output"], description: "data.output" },
26
- { path: ["data", "image", "url"], description: "data.image.url (birefnet/rembg)" },
27
- { path: ["data", "images", "0", "url"], description: "data.images[0].url (flux)" },
26
+ { path: ["data", "image", "url"], description: "data.image.url" },
27
+ { path: ["data", "images", "0", "url"], description: "data.images[0].url" },
28
28
  // Direct formats (no wrapper)
29
29
  { path: ["image"], description: "image (string)" },
30
30
  { path: ["imageUrl"], description: "imageUrl" },
@@ -35,10 +35,10 @@ export const IMAGE_EXTRACTION_RULES: readonly ExtractionRule[] = [
35
35
 
36
36
  /**
37
37
  * Video extraction rules - checked in order, first success wins
38
- * Supports: FAL.ai wrapper, direct formats, nested objects, arrays
38
+ * Supports various response wrapper formats and direct formats
39
39
  */
40
40
  export const VIDEO_EXTRACTION_RULES: readonly ExtractionRule[] = [
41
- // FAL.ai data wrapper formats
41
+ // Data wrapper formats
42
42
  { path: ["data", "video"], description: "data.video (string)" },
43
43
  { path: ["data", "videoUrl"], description: "data.videoUrl" },
44
44
  { path: ["data", "video_url"], description: "data.video_url" },
@@ -14,7 +14,6 @@ export type ImageResultExtractor = (result: unknown) => string | undefined;
14
14
  /**
15
15
  * Extract image URL from AI generation result
16
16
  * Uses Chain of Responsibility pattern with declarative rules
17
- * Supports: FAL.ai wrapper, birefnet, rembg, flux, and direct formats
18
17
  */
19
18
  export const extractImageResult: ImageResultExtractor = (result) => {
20
19
  return executeRules(result, IMAGE_EXTRACTION_RULES, "ImageExtractor");
@@ -14,7 +14,6 @@ export type VideoResultExtractor = (result: unknown) => string | undefined;
14
14
  /**
15
15
  * Extract video URL from AI generation result
16
16
  * Uses Chain of Responsibility pattern with declarative rules
17
- * Supports: FAL.ai wrapper, direct formats, nested objects, arrays
18
17
  */
19
18
  export const extractVideoResult: VideoResultExtractor = (result) => {
20
19
  return executeRules(result, VIDEO_EXTRACTION_RULES, "VideoExtractor");
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * useDualImageGeneration Hook
3
3
  * Generic hook for dual-image AI generation workflows
4
- * Sends image_urls array as required by FAL AI multi-image models
5
4
  */
6
5
 
7
6
  import { useState, useCallback, useMemo } from "react";