Aller au contenuAller à la navigationAller au contact

Applications

WinCheck

Avez-vous gagné ? Scannez le ticket.

WinCheck lit un ticket Swiss Loto, Euromillions ou EuroDreams avec l’appareil photo, va chercher le tirage et affiche le rang et le gain, grille par grille. Le ticket, lui, ne quitte pas le téléphone.

Un ticket de loterie appartient à celui qui le présente. Sa photo et ses numéros sont donc des données sensibles : la lecture se fait entièrement sur l’appareil, et seuls le jeu et la date du tirage partent sur le réseau.

Deux applications natives, iPhone en SwiftUI et Android en Kotlin, sans code partagé. Elles suivent la même spécification des règles de jeu et rejouent les mêmes cas de test.

Pour
Joueurs de la Loterie Romande
Plateformes
  • iPhone
  • Android
Technologies
  • Swift
  • SwiftUI
  • Vision
  • Kotlin
  • Jetpack Compose
  • ML Kit

Le ticket ne quitte jamais le téléphone.

WinCheck sur iPhone et sur Android

Photographier, relire, vérifier

On cadre le ticket et on déclenche. Si l’image est floue, l’app attend la mise au point. Elle lit ensuite le jeu, la date du tirage et les grilles jouées, avec le Joker, le Replay, le Swiss Win ou le Super Star.

Avant de comparer, elle montre ce qu’elle a compris. Une grille douteuse est marquée « À vérifier », à côté de la bande de photo d’où elle vient. Un numéro se corrige au clavier, et un ticket illisible se termine à la main, sans perdre ce qui a déjà été lu.

Le cadre de visée
La vérification d’un ticket, corrigé au clavier
Le gain, grille par grille

Lire plusieurs fois

Une seule lecture ne suffit pas pour un papier thermique froissé. WinCheck recadre le ticket, puis le relit : redressé, réduit, éclairage égalisé, contraste renforcé, découpé en morceaux. Il s’arrête dès que le nombre de grilles annoncé sur le ticket est atteint et confirmé par deux lectures.

Les lectures votent ensuite, grille par grille, et une grille sur laquelle elles divergent est signalée. La date est contrôlée à part : ce doit être un jour de tirage du jeu, juste après la date d’achat. Sur un banc de 17 tickets réels, chaque grille mal lue a été signalée, sur iPhone comme sur Android.

Les lectures successives d’un ticket, puis le vote

Pas de modèle de langage

Un modèle de langage de 400 Mo a été essayé pour interpréter le texte lu. Il a été écarté : les erreurs venaient de la lecture des pixels, pas de leur interprétation.

Le gain se calcule par des règles, écrites une fois dans la spécification, et le montant vient toujours du tirage officiel. Une rente EuroDreams ou un gain sans montant fixe s’affiche comme tel, jamais comme « 0 CHF ».

Un seul serveur

L’app ne parle qu’au serveur des tirages publiés par la Loterie Romande, et n’y envoie que le jeu et la date. Toute requête vers une autre adresse est bloquée avant de partir, et des tests le vérifient sur les deux plateformes.

Aucune image n’est gardée. Un ticket n’est enregistré que si on le demande, dans une base chiffrée sur Android et protégée par le chiffrement du système sur iPhone. Pas de compte, pas de cloud, pas de suivi.

network/LoRoClient.kt
companion object {
    const val ALLOWED_HOST = "jeux.loro.ch"
    const val PRODUCTION_BASE_URL = "https://$ALLOWED_HOST/api/dbg"
}

private val baseUrl: HttpUrl = baseUrl.toHttpUrl()

private val httpClient: OkHttpClient = httpClient.newBuilder()
    .addInterceptor { chain ->
        val host = chain.request().url.host
        check(host == this.baseUrl.host) {
            "Requête interdite vers $host : seul ${this.baseUrl.host} est autorisé."
        }
        chain.proceed(chain.request())
    }
    .build()
WinCheckTests/LoRoClientTests.swift
@Test func fullVerificationOnlyTalksToLoRo() async throws {
    AuditURLProtocol.recordedHosts = []
    URLProtocol.registerClass(AuditURLProtocol.self)
    defer { URLProtocol.unregisterClass(AuditURLProtocol.self) }

    let repository = DrawRepository(
        fetcher: LoRoClient(),
        cacheDirectory: FileManager.default.temporaryDirectory
            .appendingPathComponent("audit-\(UUID().uuidString)"))
    let verifier = TicketVerifier(repository: repository)
    let ticket = try await verifier.verify(
        gameId: .swissLoto,
        drawDateISO: "2026-07-01",
        grids: [PlayedGrid(mainNumbers: [3, 6, 16, 20, 32, 40], secondaryNumbers: [5])])

    #expect(ticket.status == .pendingDraw)
    #expect(!AuditURLProtocol.recordedHosts.isEmpty)
    #expect(Set(AuditURLProtocol.recordedHosts) == [LoRoClient.allowedHost])
}

Le tirage, même plus tard

Un ticket joué pour un tirage à venir attend dans « Mes tickets ». Le jour du tirage, dès 22 heures à l’heure suisse, l’app le vérifie toute seule, puis toutes les heures jusqu’à la publication des résultats, et prévient d’une notification.

Un ticket de plus de six mois est signalé comme trop ancien : les gains y sont prescrits.

Mes tickets : gagné, perdu, en attente
La notification d’un résultat

Deux applications, une seule spécification.

Jeux, avec leurs jeux annexes
3
Jeux, avec leurs jeux annexes
Seul serveur contacté
1
Seul serveur contacté
Image de ticket conservée
0
Image de ticket conservée
Tests en Swift et en Kotlin
240+
Tests en Swift et en Kotlin

Un projet dans la même veine ?Ou le projet que vous n’osez pas lancer.

Un gros projet se mène comme un petit : une étape à la fois. La première, c’est un café à Vevey ou un appel.

Parlons-en