Print Server API

Documentación para conectar la app Flutter de impresión con ComeYa

v1.0
Flutter Windows
Base44 Functions

Arquitectura

App Web comeya.app
→ HTTP localhost:6789
Flutter Print Server
→ API Base44
Base44 Backend

Flujo de impresión

  1. Web detecta servidor en localhost:6789
  2. Envía HTML del ticket vía POST /print
  3. Flutter imprime a la impresora térmica

Flujo de auth

  1. Admin copia el token desde Impresoras
  2. Flutter lo guarda con secure storage
  3. Llama a printServerAuth para validar
POST
https://comeya.base44.app/api/functions/printServerAuth

Autenticar el servidor de impresión y obtener configuración inicial.

Request Body

json
{
  "restaurant_id": "abc123",
  "print_server_token": "pst_xxxxxxxxxxxx"
}

Response 200

json
{
  "success": true,
  "restaurant": {
    "id": "abc123",
    "name": "Mi Restaurante"
  },
  "printers": [
    {
      "id": "p1",
      "name": "Caja Principal",
      "printer_name": "EPSON TM-T20",
      "type": "ticket",
      "paper_width": 80,
      "auto_cut": true,
      "copies": 1,
      "trigger_on": ["order_received"]
    }
  ]
}
POST
https://comeya.base44.app/api/functions/printServerConfig

Obtener configuración actualizada y enviar heartbeat. Llamar cada 5 minutos.

Request Body

json
{
  "restaurant_id": "abc123",
  "print_server_token": "pst_xxxx",
  "server_port": 6789,
  "version": "1.0.0"
}

Response 200

json
{
  "success": true,
  "printers": [...],
  "ticket_header": "Bienvenido!",
  "ticket_footer": "Gracias por visitarnos"
}
POST
https://comeya.base44.app/api/functions/(local) localhost:6789/print

Endpoint LOCAL del servidor Flutter. La app web llama esto directamente.

json
// Request desde comeya.app → localhost:6789/print
{
  "type": "ticket",     // "ticket" | "kitchen" | "bar"
  "content": "<html>..ticket html..</html>",
  "copies": 1
}

// Response del servidor Flutter
{ "success": true }

El usuario ingresa su Restaurant ID y Token. Guárdalos con secure_storage.

dart
// auth_service.dart
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthService {
  static const _storage = FlutterSecureStorage();
  static const _baseUrl = 'https://comeya.base44.app/api/functions';

  static Future<Map<String, dynamic>> login({
    required String restaurantId,
    required String printServerToken,
  }) async {
    final dio = Dio();
    final response = await dio.post(
      '$_baseUrl/printServerAuth',
      data: {
        'restaurant_id': restaurantId,
        'print_server_token': printServerToken,
      },
    );

    if (response.statusCode == 200) {
      final data = response.data;
      // Guardar credenciales localmente
      await _storage.write(key: 'restaurant_id', value: restaurantId);
      await _storage.write(key: 'print_server_token', value: printServerToken);
      await _storage.write(key: 'restaurant_name', value: data['restaurant']['name']);
      return data;
    }
    throw Exception(response.data['error'] ?? 'Error de autenticación');
  }

  static Future<bool> isLoggedIn() async {
    final token = await _storage.read(key: 'print_server_token');
    return token != null;
  }

  static Future<void> logout() async {
    await _storage.deleteAll();
  }
}

Servidor HTTP en localhost:6789 que recibe trabajos de impresión desde el navegador.

dart
// print_server.dart
import 'dart:convert';
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;

class PrintServer {
  static HttpServer? _server;
  static int port = 6789;

  static Future<void> start() async {
    final handler = const Pipeline()
        .addMiddleware(_corsMiddleware())
        .addHandler(_router);

    _server = await io.serve(handler, 'localhost', port);
    print('Servidor de impresión en http://localhost:$port');
  }

  static Future<void> stop() async {
    await _server?.close();
  }

  static Middleware _corsMiddleware() {
    return (Handler handler) {
      return (Request request) async {
        if (request.method == 'OPTIONS') {
          return Response.ok('', headers: _corsHeaders);
        }
        final response = await handler(request);
        return response.change(headers: _corsHeaders);
      };
    };
  }

  static const _corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
  };

  static Future<Response> _router(Request request) async {
    switch (request.url.path) {
      case 'status':
        return Response.ok(
          jsonEncode({'status': 'active', 'port': port}),
          headers: {'Content-Type': 'application/json'},
        );
      case 'print':
        return _handlePrint(request);
      default:
        return Response.notFound('Ruta no encontrada');
    }
  }

  static Future<Response> _handlePrint(Request request) async {
    final body = await request.readAsString();
    final data = jsonDecode(body) as Map<String, dynamic>;

    final type = data['type'] as String;   // ticket | kitchen | bar
    final content = data['content'] as String; // HTML del ticket
    final copies = (data['copies'] as num?)?.toInt() ?? 1;

    // Buscar impresora configurada para este tipo
    final printer = PrinterConfigCache.getByType(type);
    if (printer == null) {
      return Response(
        404,
        body: jsonEncode({'error': 'No hay impresora configurada para tipo: $type'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    // Imprimir
    await PrintingService.printHtml(
      printerName: printer['printer_name'],
      htmlContent: content,
      copies: copies,
      paperWidth: printer['paper_width'] ?? 80,
    );

    return Response.ok(
      jsonEncode({'success': true}),
      headers: {'Content-Type': 'application/json'},
    );
  }
}

Cada 5 minutos sincroniza la configuración de impresoras y envía el estado al servidor.

dart
// heartbeat_service.dart
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class HeartbeatService {
  static const _storage = FlutterSecureStorage();
  static Timer? _timer;
  static const _baseUrl = 'https://comeya.base44.app/api/functions';
  static const version = '1.0.0';

  static void start({ int port = 6789 }) {
    _timer?.cancel();
    // Enviar heartbeat cada 5 minutos
    _timer = Timer.periodic(const Duration(minutes: 5), (_) => _send(port));
    _send(port); // enviar inmediatamente
  }

  static void stop() => _timer?.cancel();

  static Future<void> _send(int port) async {
    final restaurantId = await _storage.read(key: 'restaurant_id');
    final token = await _storage.read(key: 'print_server_token');
    if (restaurantId == null || token == null) return;

    try {
      final dio = Dio();
      final response = await dio.post(
        '$_baseUrl/printServerConfig',
        data: {
          'restaurant_id': restaurantId,
          'print_server_token': token,
          'server_port': port,
          'version': version,
        },
      );
      // Actualizar config de impresoras si cambió
      if (response.data['printers'] != null) {
        PrinterConfigCache.update(response.data['printers']);
      }
    } catch (e) {
      print('Heartbeat error: $e');
    }
  }
}

Cómo detectar el servidor y enviar trabajos de impresión desde React.

javascript
// En tu app React (comeya.app)
// utils/printService.js

const PRINT_SERVER_URL = 'http://localhost:6789';

export const checkPrintServer = async () => {
  try {
    const r = await fetch(`${PRINT_SERVER_URL}/status`, {
      signal: AbortSignal.timeout(1500)
    });
    return r.ok;
  } catch {
    return false;
  }
};

export const printTicket = async (htmlContent, type = 'ticket', copies = 1) => {
  const response = await fetch(`${PRINT_SERVER_URL}/print`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ type, content: htmlContent, copies })
  });
  if (!response.ok) throw new Error('Error al imprimir');
  return response.json();
};

// Uso en componente:
// const serverActive = await checkPrintServer();
// if (serverActive) await printTicket(ticketHTML, 'kitchen');

Quick Start para el tenant

  1. 1Instalar ComeYa Print Server (el .exe generado con Flutter)
  2. 2Abrir la app → "Iniciar sesión" → Pegar el Restaurant ID y el Token
  3. 3Hacer clic en "Iniciar Servidor" → ícono verde en bandeja del sistema
  4. 4En ComeYa web → Panel → Impresoras → Configurar nombres de impresoras
  5. 5¡Listo! Los tickets se imprimirán automáticamente