No description
  • TypeScript 98.8%
  • Python 0.6%
  • JavaScript 0.4%
  • Shell 0.2%
Find a file
2026-10-07 14:34:52 +02:00
.claude Initial commit 2026-09-08 22:51:59 +02:00
.vscode Initial commit 2026-09-08 22:51:59 +02:00
__tests__ Actualizar la app sin tienda: EAS Update y el APK del servidor 2026-10-07 14:27:18 +02:00
assets/images Avisos al telefono y el icono es el de la web 2026-09-14 11:42:32 +02:00
scripts Avisos al telefono y el icono es el de la web 2026-09-14 11:42:32 +02:00
src Actualizar la app sin tienda: EAS Update y el APK del servidor 2026-10-07 14:27:18 +02:00
.env.example Pestañas, marcador, y las fotos por fin a la vista: la tanda C1 2026-09-10 12:20:08 +02:00
.gitignore Avisos al telefono y el icono es el de la web 2026-09-14 11:42:32 +02:00
AGENTS.md Initial commit 2026-09-08 22:51:59 +02:00
app.json Version 0.3.0 2026-10-07 14:34:52 +02:00
CLAUDE.md Initial commit 2026-09-08 22:51:59 +02:00
eas.json Actualizar la app sin tienda: EAS Update y el APK del servidor 2026-10-07 14:27:18 +02:00
eslint.config.js La cola se vacía: lo que llega al servidor deja de vivir aquí 2026-09-09 17:48:18 +02:00
google-services.json Avisos al telefono y el icono es el de la web 2026-09-14 11:42:32 +02:00
HOJA_DE_RUTA.md Actualizar la app sin tienda: EAS Update y el APK del servidor 2026-10-07 14:27:18 +02:00
jest.resolver.js App terminada a falta de tanda de publicacion 2026-09-11 19:12:27 +02:00
jest.setup.js App terminada a falta de tanda de publicacion 2026-09-11 19:12:27 +02:00
LICENSE Primero 2026-09-08 23:31:40 +02:00
package-lock.json Actualizar la app sin tienda: EAS Update y el APK del servidor 2026-10-07 14:27:18 +02:00
package.json Actualizar la app sin tienda: EAS Update y el APK del servidor 2026-10-07 14:27:18 +02:00
README.md Avisos al telefono y el icono es el de la web 2026-09-14 11:42:32 +02:00
tsconfig.json Perfiles y el primer test de este lado: la tanda C2a 2026-09-10 12:58:39 +02:00

Techos de España · app móvil

App para iOS y Android del reto de las 152 cimas provinciales. Es el hito H10 de la hoja de ruta del proyecto, y vive en su propio repositorio a propósito: aquí hay node_modules, paso de compilación y medio internet descargado; el servidor sigue arrancando con docker compose up y sin nada que construir.

La frontera entre los dos repositorios es la API. De este lado no se copia código del servidor ni datos del catálogo: las cimas las pide la app a /api/v1/picos en tiempo de ejecución. Ver el LICENSE, donde eso es además una condición y no una recomendación.

Qué hace falta

  • Node 22 LTS y npm.
  • La app Expo Go en el teléfono, desde su tienda. Es el entorno de pruebas: no hacen falta ni Xcode ni Android Studio.
  • Un servidor de pruebas con la API viva. No se desarrolla contra producción: la app escribe cumbres de verdad.
  • Para los avisos al teléfono, un development build con EAS. Expo Go ya no recibe push remoto (salió de allí en el SDK 53), así que esa parte —y solo esa— no se puede probar con el QR. Ver «Avisos al teléfono», abajo.

Arrancar

npm install
cp .env.example .env.local     # y apuntar EXPO_PUBLIC_API_URL al servidor de pruebas
npx expo start                 # y escanear el QR con Expo Go

Se entra desde la propia app: un botón, el navegador del teléfono, Google, y de vuelta. La llave que sale de ahí se guarda en el almacén seguro —Keychain en iOS, preferencias cifradas en Android— y no se enseña en ninguna pantalla.

En Expo Go hay que decírselo al servidor. La dirección de vuelta no es picosmovil://vinculado sino exp://192.168.0.100:8082/--/vinculado, con la IP de la máquina que corre Metro y el puerto que le tocara ese día —Expo coge el 8082 si el 8081 está ocupado—. Esa dirección entera va en VUELTAS_DE_PRUEBA en el .env del servidor, separada por comas si hay más de una. Es una lista de direcciones literales y no un prefijo exp:// a propósito: lo que se abre es una dirección, no una categoría.

Cómo está repartido

src/api/       el cliente de la API: la llave, los tipos, las llamadas y cómo
               piden datos las pantallas (`carga.ts`)
src/datos/     lo que vive en el teléfono: SQLite, el catálogo, la cola y el GPS
src/app/       las pantallas (expo-router; el fichero es la ruta)
src/ui/        lo que se repite en varias pantallas: la cara, las miniaturas,
               el visor de fotos y el mapa
src/tema.ts    cuatro colores, dos modos y cómo se dicen las fechas

Las pantallas van en dos alturas. La pila de raíz (src/app/_layout.tsx) tiene las dos mitades de la app —fuera: entrar; dentro: todo lo demás— y el modal de apuntar, que es lo único que interrumpe. Dentro cuelga (app), que son las tres pestañas, y cada pestaña lleva su propia pila, para que lo que se abra desde una se abra dentro de ella y con la barra puesta. Solo el cuaderno va entre paréntesis: un grupo no sale en la dirección, así que su index es /.

src/api/tipos.ts es un espejo a mano de los esquemas de app/api.py del servidor. Si allí cambia un campo, aquí no se enterará nadie hasta que algo se pinte vacío: el día que duela, se generan desde /api/v1/openapi.json.

Cómo se entra, en cinco pasos

Ni el código ni la llave pasan nunca por una URL, y es lo único importante de todo el mecanismo. Es el mismo truco que OAuth llama PKCE:

  1. La app sortea 32 bytes —el verificador—, los guarda en el almacén seguro y calcula su SHA-256, el reto.
  2. Abre el navegador del sistema en /movil/vincular?reto=…&nombre=…&vuelta=…. El navegador del sistema y no una vista dentro de la app: es lo que exige Google y lo que impide que esta app pueda ver la contraseña de nadie.
  3. Allí se entra —o se crea la cuenta— y se confirma qué teléfono se está conectando. La vinculación se crea en el POST de ese botón, nunca al abrir la página.
  4. Se vuelve a la app con un identificador opaco, que no abre nada.
  5. La app llama a POST /api/v1/vinculacion/canje con {estado, verificador} y recibe la llave en el cuerpo de la respuesta.

Cualquier otra aplicación del teléfono puede haber registrado el mismo esquema y quedarse con la vuelta: no se lleva nada, porque el verificador no ha salido de aquí. Y «Salir de este teléfono», en los ajustes, corta esa llave en el servidor además de borrarla de aquí: los demás dispositivos siguen dentro.

Estado

Las tandas son las del H10.

Tanda Qué es Estado
A Entrar sin copiar una llave a mano hecha y probada en el móvil (9-9-2026)
B Apuntar desde la cima: cámara, GPS y cola escrita entera (B1, B2 y B3, 9-9-2026). Falta apuntar una cumbre de verdad desde el teléfono para cerrar el «hecho cuando»
C Leer: marcador, perfiles, catálogo y mapa hecha (11-9-2026)
D Escribir: lo social, clubes y ajustes hecha (11-9-2026)
— Arreglos: visor de fotos, ficha de cima entera y el mapa, que en Android salía vacío hechos (11-9-2026)
— Avisos al teléfono (H13 del servidor) escritos (13-9-2026). Falta el development build para probarlos en un móvil de verdad
E Publicar en las dos tiendas pendiente

Si algo se cae, que sea la D y no la B.

El mapa es Leaflet con teselas de OpenStreetMap dentro de un WebView, el mismo que la web, y no react-native-maps: el de Android era el de Google y no pinta nada sin una clave de Google Cloud. El porqué entero está en src/ui/Mapa.tsx.

Avisos al teléfono

Tres y ninguno más: te piden seguirte, alguien a quien sigues apunta una cumbre, te comentan una salida. Se apagan uno a uno en Ajustes → Avisos, y el ajuste es de la cuenta: apagarlo lo apaga en todos tus teléfonos.

El permiso se pide en esa pantalla y nunca al arrancar. En iOS el diálogo del sistema sale una vez en la vida de la instalación, así que gastarlo en un arranque —antes de explicar qué se va a avisar— es perderlo para siempre.

Para probarlos hace falta compilar, una vez:

npx eas-cli@latest login      # una vez: sin esto, lo demás dice «Not logged in»
npx eas-cli@latest init       # escribe extra.eas.projectId en app.json
npx eas-cli@latest build --profile development --platform android

El paquete es eas-cli y el binario eas: npx eas init falla con «could not determine executable to run» mientras no esté instalado. Con npm install -g eas-cli basta eas init.

Y las credenciales, que hay que hacer a mano en las dos consolas: en Android, una service account key de FCM v1 desde Firebase; en iOS, una clave de APNs, que exige la cuenta de desarrollador de Apple (99 $/año). Sin projectId la app no se cae: la pantalla de ajustes dice que este build no puede recibir avisos.

El detalle entero —qué lleva dentro un aviso y qué no, y quién lo recibe— está en el §9 de HOJA_DE_RUTA.md y en el README del servidor.

Avisos que cuestan tiempo si se descubren tarde

  • No corras npm audit fix --force. Expo fija versiones probadas entre sí; forzarlo las desancla y rompe la compilación. El chequeo bueno es npx expo-doctor.
  • Todo lo que empieza por EXPO_PUBLIC_ viaja dentro del binario, en claro. Aquí no se guardan secretos: la llave de la API la obtiene cada teléfono para sí mismo.
  • eas init toca app.json y no solo para escribir el projectId. El 13-9-2026 añadió además un bloque android.permissions que no hacía falta: los permisos los declaran los plugins, y el bloque solo repetía lo que ya estaba. Si vuelve a salir, se quita. Lo que sí hay que revisar de vez en cuando es la lista de verdad, con npx expo config --type introspect: la app pide cámara, ubicación, almacenamiento e internet, y nada más. RECORD_AUDIO lo metía expo-image-picker por defecto y está apagado a propósito con microphonePermission: false —esta app nunca graba audio, y un permiso de micrófono en la ficha de la tienda hay que explicarlo—.
  • El nombre y el identificador de aplicación (com.…) se eligen una sola vez. Los de hoy son provisionales y se cambian libremente hasta el día que se manda la primera versión a revisar, y ni un minuto después. Ver el §8 de la hoja de ruta.
  • SDK 57 cambió cosas. Los docs de esta versión están en https://docs.expo.dev/versions/v57.0.0/; expo-file-system tiene API nueva (File/Directory) y la de siempre vive en expo-file-system/legacy.

Licencia

Código público, no software libre: todos los derechos reservados. Se publica para que cualquiera pueda auditar qué hace la app con la cámara y con la ubicación. Ver LICENSE.