Submission Flow

Learn how to validate, prepare, and submit documents to the MIMS API with proper error handling and user feedback.

Submission Overview

The submission flow consists of four phases:

  1. Validation - Check all required fields are completed
  2. Payload Generation - Build the submission payload
  3. API Request - Send the payload to the server
  4. Response Handling - Process success or handle errors

Complete Submission Implementation

1

Validate Fields

Before submission, validate all fields meet requirements:

hooks/useSubmission.tstsx
import { useDocumentStore, useSDK } from '@mims/sdk-react';
import { useState, useCallback } from 'react';

export function useSubmission() {
  const { apiRequest } = useSDK();
  const validateFields = useDocumentStore((s) => s.validateFields);
  const getSubmissionPayload = useDocumentStore((s) => s.getSubmissionPayload);
  const fields = useDocumentStore((s) => s.fields);

  const [status, setStatus] = useState<'idle' | 'validating' | 'submitting' | 'success' | 'error'>('idle');
  const [error, setError] = useState<string | null>(null);
  const [submissionId, setSubmissionId] = useState<string | null>(null);

  const getValidationSummary = useCallback(() => {
    const allFields = Array.from(fields.values());
    const requiredFields = allFields.filter(f => f.definition.required);
    const completedRequired = requiredFields.filter(f => f.value !== null);
    const fieldsWithErrors = allFields.filter(f => f.errors.length > 0);

    return {
      totalFields: allFields.length,
      requiredFields: requiredFields.length,
      completedRequired: completedRequired.length,
      fieldsWithErrors: fieldsWithErrors.length,
      isComplete: completedRequired.length === requiredFields.length,
      progress: requiredFields.length > 0 
        ? (completedRequired.length / requiredFields.length) * 100 
        : 100,
    };
  }, [fields]);

  return { status, error, submissionId, getValidationSummary };
}
2

Build Submission Payload

The SDK provides a method to generate the submission payload:

Payload Structuretsx
// Generated by getSubmissionPayload()
interface DocumentSubmissionPayload {
  documentId: string;
  submittedAt: string;  // ISO 8601 timestamp
  renderContext: {
    scale: number;
    viewportWidth: number;
    viewportHeight: number;
    devicePixelRatio: number;
  };
  pages: Array<{
    pageNumber: number;
    width: number;
    height: number;
    rotation: number;
  }>;
  fields: Array<{
    id: string;
    type: FieldType;
    page: number;
    position: { x: number; y: number; width: number; height: number };
    value: AnyFieldValue;
    style?: { fontSize?: number; fontFamily?: string; textAlign?: string };
  }>;
  checksum: string;  // Integrity verification hash
}
3

Submit to API

Send the payload using the authenticated API request:

Submit Functiontsx
const submit = useCallback(async () => {
  setStatus('validating');
  setError(null);

  // Run validation
  const isValid = validateFields();
  
  if (!isValid) {
    setStatus('error');
    setError('Please complete all required fields');
    return { success: false, error: 'Validation failed' };
  }

  // Generate payload
  const payload = getSubmissionPayload();
  
  if (!payload) {
    setStatus('error');
    setError('No document loaded');
    return { success: false, error: 'No document' };
  }

  setStatus('submitting');

  try {
    const result = await apiRequest<SubmissionResponse>('/sdk/documents/submit', {
      method: 'POST',
      body: JSON.stringify(payload),
    });

    setStatus('success');
    setSubmissionId(result.submissionId);
    
    return { success: true, submissionId: result.submissionId };
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Submission failed';
    setStatus('error');
    setError(message);
    
    return { success: false, error: message };
  }
}, [validateFields, getSubmissionPayload, apiRequest]);
4

Handle Response

Process the server response and update UI accordingly:

Response Handlingtsx
interface SubmissionResponse {
  success: boolean;
  submissionId: string;
  documentId: string;
  timestamp: string;
  message?: string;
}

// In your component
function SubmitButton({ onSuccess }: { onSuccess?: (id: string) => void }) {
  const { submit, status, error, submissionId } = useSubmission();

  const handleClick = async () => {
    const result = await submit();
    
    if (result.success && result.submissionId) {
      onSuccess?.(result.submissionId);
    }
  };

  return (
    <div>
      <button 
        onClick={handleClick}
        disabled={status === 'validating' || status === 'submitting'}
        className="px-4 py-2 bg-emerald-500 text-white rounded disabled:opacity-50"
      >
        {status === 'validating' && 'Validating...'}
        {status === 'submitting' && 'Submitting...'}
        {status === 'idle' && 'Submit Document'}
        {status === 'success' && 'Submitted!'}
        {status === 'error' && 'Try Again'}
      </button>
      
      {error && (
        <p className="text-red-500 text-sm mt-2">{error}</p>
      )}
      
      {status === 'success' && (
        <p className="text-emerald-500 text-sm mt-2">
          Submission ID: {submissionId}
        </p>
      )}
    </div>
  );
}

Pre-Submission Validation UI

Show users which fields need attention before they can submit:

ValidationSummary.tsxtsx
function ValidationSummary() {
  const fields = useDocumentStore((s) => s.fields);
  const goToPage = useDocumentStore((s) => s.goToPage);
  const selectField = useDocumentStore((s) => s.selectField);

  const incompleteRequired = Array.from(fields.values())
    .filter(f => f.definition.required && !f.value);

  if (incompleteRequired.length === 0) {
    return (
      <div className="p-4 bg-emerald-50 border border-emerald-200 rounded-lg">
        <p className="text-emerald-700 font-medium">
          All required fields are complete. Ready to submit!
        </p>
      </div>
    );
  }

  return (
    <div className="p-4 bg-amber-50 border border-amber-200 rounded-lg">
      <p className="text-amber-700 font-medium mb-3">
        {incompleteRequired.length} required field(s) need attention:
      </p>
      <ul className="space-y-2">
        {incompleteRequired.map((field) => (
          <li key={field.definition.id}>
            <button
              onClick={() => {
                goToPage(field.definition.page);
                selectField(field.definition.id);
              }}
              className="text-amber-600 hover:text-amber-800 underline text-sm"
            >
              {field.definition.label || field.definition.id} (Page {field.definition.page})
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Retry Logic

Implement automatic retry for transient failures:

Retry Implementationtsx
async function submitWithRetry(
  payload: DocumentSubmissionPayload,
  apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>,
  maxRetries = 3
): Promise<SubmissionResponse> {
  let lastError: Error | null = null;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await apiRequest<SubmissionResponse>('/sdk/documents/submit', {
        method: 'POST',
        body: JSON.stringify(payload),
      });
    } catch (error) {
      lastError = error instanceof Error ? error : new Error('Unknown error');
      
      // Don't retry on validation errors (4xx)
      if (lastError.message.includes('400') || lastError.message.includes('422')) {
        throw lastError;
      }

      // Wait before retrying (exponential backoff)
      if (attempt < maxRetries) {
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  throw lastError ?? new Error('Max retries exceeded');
}

Submission Idempotency

The submission endpoint is idempotent based on the checksum. If you retry with the same payload, you'll receive the original submission ID rather than creating a duplicate.

Offline Support

Queue submissions when offline and retry when connectivity returns:

Offline Queuetsx
// Simple offline queue using localStorage
const QUEUE_KEY = 'mims_submission_queue';

function queueSubmission(payload: DocumentSubmissionPayload) {
  const queue = JSON.parse(localStorage.getItem(QUEUE_KEY) || '[]');
  queue.push({
    payload,
    queuedAt: new Date().toISOString(),
    attempts: 0,
  });
  localStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
}

async function processQueue(apiRequest: ApiRequestFn) {
  const queue = JSON.parse(localStorage.getItem(QUEUE_KEY) || '[]');
  const remaining = [];

  for (const item of queue) {
    try {
      await apiRequest('/sdk/documents/submit', {
        method: 'POST',
        body: JSON.stringify(item.payload),
      });
    } catch (error) {
      item.attempts++;
      if (item.attempts < 5) {
        remaining.push(item);
      }
    }
  }

  localStorage.setItem(QUEUE_KEY, JSON.stringify(remaining));
}

// Listen for online event
if (typeof window !== 'undefined') {
  window.addEventListener('online', () => {
    processQueue(apiRequest);
  });
}

Success Confirmation

Show a confirmation screen after successful submission:

SubmissionConfirmation.tsxtsx
interface SubmissionConfirmationProps {
  submissionId: string;
  documentId: string;
  onNewDocument?: () => void;
  onViewSubmission?: (id: string) => void;
}

function SubmissionConfirmation({
  submissionId,
  documentId,
  onNewDocument,
  onViewSubmission,
}: SubmissionConfirmationProps) {
  return (
    <div className="max-w-md mx-auto text-center py-12">
      <div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-6">
        <CheckIcon className="w-8 h-8 text-emerald-600" />
      </div>
      
      <h2 className="text-2xl font-bold mb-2">Document Submitted!</h2>
      <p className="text-neutral-600 mb-6">
        Your document has been successfully submitted and is being processed.
      </p>
      
      <div className="bg-neutral-50 rounded-lg p-4 mb-6 text-left">
        <dl className="space-y-2 text-sm">
          <div className="flex justify-between">
            <dt className="text-neutral-500">Submission ID</dt>
            <dd className="font-mono">{submissionId}</dd>
          </div>
          <div className="flex justify-between">
            <dt className="text-neutral-500">Document ID</dt>
            <dd className="font-mono">{documentId}</dd>
          </div>
          <div className="flex justify-between">
            <dt className="text-neutral-500">Submitted</dt>
            <dd>{new Date().toLocaleString()}</dd>
          </div>
        </dl>
      </div>
      
      <div className="flex gap-3 justify-center">
        {onViewSubmission && (
          <button
            onClick={() => onViewSubmission(submissionId)}
            className="px-4 py-2 bg-emerald-500 text-white rounded-lg"
          >
            View Submission
          </button>
        )}
        {onNewDocument && (
          <button
            onClick={onNewDocument}
            className="px-4 py-2 border border-neutral-300 rounded-lg"
          >
            New Document
          </button>
        )}
      </div>
    </div>
  );
}

Related