# De Android-app De app is dezelfde applicatie als de website, met dezelfde schermen. Wat verschilt, is de weg naar de gegevens: de website praat rechtstreeks met MariaDB, de app praat via de SocketServer. ## Hoe het in elkaar zit | Project | Wat het is | Te testen? | |---|---|---| | `MijnBesluit.Client` | De aanroepen over de socket, met modellen voor de app. Geen regels, alleen vertalen. | Ja — 10 controles in de zelftest, tegen een echte socketserver | | `MijnBesluit.AppUi` | De schermen (Blazor). Inloggen, overzicht, kluis, familie, momenten, nalatenschap. | Compileert; het gedrag zit grotendeels in de laag eronder | | `MijnBesluit.App` | De Android-schil: één webview met de schermen erin. | Nee — dit moet in Visual Studio op een toestel | De schermen staan bewust in een eigen project en niet in de app zelf. Zo kan de website ze later overnemen, en hoeft een wijziging maar op één plek. ## Wat je nodig hebt in Visual Studio De MAUI-werkbelasting (".NET Multi-platform App UI development"). Heb je die al voor RenHDashboardMaui, dan ben je er al. Het project staat op `net10.0-android`, net als dat project. Klopt je versie niet, pas dan `TargetFrameworks` in `src\MijnBesluit.App\MijnBesluit.App.csproj` aan naar wat je wél hebt staan; de andere projecten hoeven niet mee te veranderen. ## Eerste keer draaien 1. Rechtermuis op `MijnBesluit.App` → **Set as Startup Project**. 2. Kies bovenin een Android-emulator of je eigen toestel. 3. F5. Verschijnt bij het bouwen een melding dat `Microsoft.AspNetCore.Components.WebView.Maui` ontbreekt, dan levert jouw werkbelasting die niet vanzelf. Voeg hem dan toe via **Manage NuGet Packages** op `MijnBesluit.App`. ## Waar de app verbinding mee zoekt In `MauiProgram.cs` staat bovenaan: ```csharp public const string Server = "mijnbesluit.nl"; public const int Poort = 5200; ``` - **Op een emulator, met de server op je eigen pc:** zet `Server` op `10.0.2.2` — dat is vanuit de emulator jouw pc — en zet `tls: true` om naar `tls: false` zolang er geen certificaat is. - **Op een echt toestel:** laat `mijnbesluit.nl` staan. ## Een APK maken ```powershell D:\MijnBesluit VS\MijnBesluit\tools\Apk-maken.ps1 -Snel # snel, om zelf te proberen D:\MijnBesluit VS\MijnBesluit\tools\Apk-maken.ps1 # ondertekend, om weg te geven D:\MijnBesluit VS\MijnBesluit\tools\Apk-maken.ps1 -Aab # bundel voor de Play Store ``` De debug-versie is meteen te installeren op je eigen toestel (wel eerst "installeren uit onbekende bron" toestaan). Die wordt gebouwd met `-p:EmbedAssembliesIntoApk=true`: bij Debug laat MAUI de .NET-assemblies anders buiten het pakket, omdat Visual Studio ze bij het debuggen los naar het toestel duwt. Zonder die instelling installeert de APK wel, maar klapt de app bij het starten meteen dicht. De release-versie vraagt de eerste keer om een sleutelbestand aan te maken; dat komt in `D:\MijnBesluitSleutels`. **Bewaar dat sleutelbestand apart, net als de mastersleutel van de kluis.** Android herkent een app aan de sleutel waarmee hij is ondertekend. Raak je hem kwijt, dan is er geen nieuwe versie meer uit te brengen die op de toestellen van je gebruikers over de oude heen gaat — ook niet met hulp van Google. Dan moet iedereen de app verwijderen en opnieuw installeren, en is alles wat lokaal op het toestel stond weg. Voor de Play Store lever je geen APK aan maar een AAB (Android App Bundle): `-Aab`. Dat is dezelfde opdracht met een ander pakketformaat, hetzelfde sleutelbestand en hetzelfde wachtwoord. Google maakt uit die bundel per toestel een APK, zodat niemand de onderdelen voor andere toestellen meedownloadt. Een AAB kun je niet zelf op een telefoon zetten. Voor het uitproberen en voor het rondsturen aan je eigen testers blijf je dus de APK gebruiken; de AAB is alleen voor de Play Console. ## De serverkant Staat klaar sinds 14 september 2026: - De SocketServer draait als Windows-service op mijnbesluit.nl, poort 5200. - Poort 5200 is open; `Test-NetConnection mijnbesluit.nl -Port 5200` geeft True. - TLS 1.3, met het certificaat van mijnbesluit.nl uit de opslag WebHosting (`B1BAFBB8A37A2899A2706FE037DDAB0F1694EFD1`, geldig tot 1 april 2027). - Database en e-mail werken via de socket: `SocketServer-proef.ps1 -Email ` levert een inlogcode in het postvak. Controleer het met: ```powershell D:\MijnBesluit VS\MijnBesluit\tools\SocketServer-proef.ps1 D:\MijnBesluit VS\MijnBesluit\tools\SocketServer-proef.ps1 -Email jouw@adres.nl ``` Let op de vervaldatum van het certificaat: bij vernieuwing verandert de vingerafdruk en moet `Socket:CertificateThumbprint` mee, gevolgd door `Restart-Service MijnBesluitSocket`. De service zet bij het opstarten in het logboek in welke opslag hij het certificaat vond en tot wanneer het geldig is. ## Wat de app op het toestel bewaart Alleen het sessietoken, in de Keystore van Android (`ToestelTokenopslag`). Geen kluisinhoud, geen wachtwoorden: die worden opgehaald op het moment dat je ze bekijkt, en zijn weg zodra je het scherm sluit. Uitloggen wist het token van het toestel. ## De terugknop van het toestel Android heeft een eigen terugknop, en die sloot standaard de hele app — ook midden in een formulier. Dat loopt nu via `Terugknop` in `MijnBesluit.AppUi`. Hoe het werkt: wie iets open heeft staan meldt zich daar aan en weer af zodra het dicht is. De laatste aanmelding is als eerste aan de beurt, dus in de volgorde waarin de dingen over elkaar heen liggen. In de praktijk: 1. Staat er een foto open in een bijlagenvenster, dan gaat die dicht. 2. Staat er een wijzigscherm open, dan gaat dat dicht — zonder op te slaan, net als Annuleren. 3. Zit je in een scherm, dan ga je naar het overzicht. 4. Ben je op het overzicht, dan verschijnt "Nog een keer terug om te sluiten". Pas een tweede druk binnen twee seconden sluit de app. De schermen hoeven hier niets voor te doen: `AppSchil` regelt stap 3 en 4, en `Popup` regelt stap 1 en 2. Een scherm dat er later bij komt doet automatisch mee. `HoofdPagina.OnBackButtonPressed` is het enige stukje MAUI erin: dat geeft de druk door en laat Android zijn gang gaan zodra niemand hem afhandelt. ## Vanzelf op slot Achter het tandwiel in de balk staat één instelling: na hoeveel minuten stilte de app dichtgaat. `Uitlogklok` houdt bij wanneer er voor het laatst iets gebeurde; de schil vangt elke aanraking en elke toets op — die bubbelen naar de buitenste laag, dus dat is één plek — en kijkt elke halve minuut of de termijn voorbij is. **Wat er dan gebeurt, hangt af van het toestel.** | Toestel | Wat er gebeurt | Standaard | |---|---|---| | Heeft een schermslot, Android 10 of nieuwer | De app gaat op slot. De sessie blijft staan; openen gaat met vingerafdruk, gezicht of de code van de telefoon. | Aan, na 15 minuten — en "uit" bestaat daar niet | | Geen schermslot, of ouder | De app logt écht uit: het sessietoken gaat van het toestel af en er moet een nieuwe code per e-mail komen. | Uit | **Op een toestel met een schermslot kan de klok niet uit.** Die keuze staat niet in de lijst, en een nul die er van vroeger nog in staat wordt opgetild naar het kwartier - in de klok zelf, niet in het scherm, zodat ook de schil ervan uitgaat. Het slot kost daar één aanraking; uit zetten levert dus bijna niets op en betekent wel dat een gevonden telefoon een open kluis is. Zonder schermslot ligt dat andersom en mag "uit" er wél staan. **Op slot gaat de lijn dicht.** Zodra de app vergrendelt, verbreekt hij de verbinding met de SocketServer; na het ontgrendelen zet hij hem weer op. Het token blijft staan en de sessie aan de serverkant ook - die hangt aan het token en niet aan de verbinding - dus je bent gewoon weer waar je was. Waarom dat de moeite is: de server verbreekt zelf een verbinding waarover tien minuten niets gebeurt, maar dat is een terugval, geen zekerheid. Een telefoon die in een tas verdwijnt of zijn netwerk kwijtraakt, laat een half-open socket achter tot die tien minuten om zijn. Zelf dichtdoen maakt er een zekerheid van, scheelt een plek van de vijfhonderd, en zorgt dat je na het ontgrendelen een verse verbinding hebt in plaats van een die misschien nog leeft. **Bij het openen vraagt hij altijd, en hij vraagt het aan de telefoon.** Ligt er een sessie op dit toestel en kan het om een vingerafdruk vragen, dan komt dat venster - ongeacht de klok. De klok gaat over wat er gebeurt terwijl de app openstaat; dit gaat over het openen zelf. Zo is de app afsluiten en opnieuw starten geen manier om langs het slot te komen. Let op *waarop* dat gebeurt: op wat er in de beveiligde opslag van de telefoon ligt, niet op een goedkeuring van de server (`HeeftBewaardeSessieAsync`, en `BijHetOpenenAsync` in de schil). Dat was eerst andersom, en dat was fout. Het slot ging pas op de app nadat de server het token had goedgekeurd, dus een telefoon zonder netwerk - of een server die net opnieuw was gestart - stuurde je naar het inlogscherm met een vraag om je e-mailadres, terwijl je telefoon prima wist wie je was. Nu komt eerst je vingerafdruk en pas daarna de vraag of de sessie nog loopt. Zolang de app dat nog niet weet, toont de schil alleen het logo: geen scherm dat een tel later achter een slot verdwijnt, en geen scherm dat alvast gegevens gaat ophalen. Dat de sessie ook echt nog loopt, is de andere helft van hetzelfde verhaal: de SocketServer bewaart zijn sessies sinds `db/16_sessies.sql` in de database en niet meer alleen in het geheugen. Zonder dat logde elke uitrol of reboot van de server iedereen uit, en was de vingerafdruk niets waard. Zie `docs/Socketprotocol.md`, hoofdstuk Sessies. Dat verschil zit in de prijs van vergeten. Met een toestelslot kost het je één aanraking, dus mag het standaard aan. Zonder kost het een e-mail afwachten, en dat leg je niemand ongevraagd op. Wie zélf "uit" kiest houdt uit: die nul staat dan in de opslag en is iets anders dan niets gekozen hebben. Er is een zelftest die dat verschil bewaakt. `IToestelslot` vraagt het toestel om iemand te controleren; `Toestelslot` in MijnBesluit.App doet dat met `BiometricPrompt` en `SetDeviceCredentialAllowed(true)`, zodat de code van de telefoon altijd als terugval werkt. **De app krijgt alleen ja of nee terug.** Er is geen eigen pincode, er wordt niets bewaard en er valt hier dus ook niets te lekken; de controle gebeurt in een apart stuk hardware waar geen enkele app bij kan. `SetDeviceCredentialAllowed` staat vanaf Android 11 als verouderd gemarkeerd. De opvolger (`SetAllowedAuthenticators`) bestaat pas vanaf 11, en de app werkt vanaf 10. Zolang dat zo is hoort deze hier te staan; gaat de ondergrens ooit naar 11, dan mag het om. Op slot laat de schil **niets** van de app meer zien — geen balk, geen navigatie, geen inhoud — en slikt de terugknop van het toestel. Een grendel die je nog door het raam laat kijken is geen grendel. Op het slotscherm staat ook "toch helemaal uitloggen", voor wie de telefoon uit handen geeft. Twee dingen die verder met opzet zo zijn: - **Er wordt op de wandklok gerekend, niet op een teller.** Een telefoon die een uur in een tas zit telt dus net zo goed mee, ook al heeft de lus in de schil dan niet gedraaid. Bij het terugkomen uit de achtergrond seint `App.xaml.cs` de klok meteen in, vóór er gegevens worden opgehaald — dat is juist het geval waar dit voor bedoeld is. Stond de app al op slot, dan komt het venster van het toestel meteen weer in beeld. - De termijn staat in de gewone voorkeursopslag van Android (`Toestelinstellingen`), niet in de Keystore: daar hoort alleen in wat geheim is, en hoe lang de app wacht is dat niet. Het sessietoken staat er wél in (`ToestelTokenopslag`), en dat is ook precies wat achter dit slot blijft liggen terwijl de app dicht is. ### Een sessie gaat er alleen af als de server hem weigert Een app die op slot staat, staat op de achtergrond, en Android sluit zo'n app vaak gewoon af. Terugkomen is dan geen ontwaken maar een koude start: de app pakt het bewaarde token op en vraagt de server wie hij is. Op dat moment is het netwerk van de telefoon soms nog niet wakker. Dat mislukken is **geen** reden om het token weg te gooien. Deed de app dat wel, dan moest je een code per e-mail aanvragen terwijl er niets mis was met je sessie — precies op het moment dat je dacht dat je alleen even je vinger hoefde te leggen. Alleen de server mag een sessie ongeldig verklaren (foutcode `sessie-verlopen`); dan ruimt `SessieKwijtAsync` het token op en gaat de app naar het inlogscherm. Blijft het token staan, dan blijft de app op zijn scherm met "geen verbinding" en probeert hij het bij de volgende handeling opnieuw. De zelfcontrole "App: geen verbinding gooit het bewaarde token niet weg" houdt beide kanten vast. ## De tegels in de lijsten Momenten, Mijn mensen, Mijn kluis en Nalatenschap tonen allemaal dezelfde tegel: * Als er een profielfoto is, staat die vooraan. Anders de beginletters, even groot, zodat een lijst niet verspringt zodra er ergens een foto bij komt. * Drie regels ernaast, altijd in dezelfde opbouw: 1. waar je op zoekt - het onderwerp, de naam, de titel. Loopt door tot de rand. 2. de toelichting, met rechts eventueel een labeltje (de status bij Mijn mensen). 3. de rest, met rechts de knoppen. Die regel mag leeg zijn; bij Mijn mensen is dat zo. * **De tegel zelf aantikken opent hem.** Bij Momenten en Nalatenschap klapt er iets uit, bij Mijn mensen en Mijn kluis opent het scherm met de gegevens. Daar staat dus geen knop voor. * Rechtsonder staan alleen de andere acties, als tekentje: de prullenbak, de envelop om iemand uit te nodigen. Een wijzertje (het driehoekje) laat zien dat er iets uitklapt; dat is geen knop. Elke knop heeft een `title` en een `aria-label`, dus een schermlezer noemt hem gewoon. Het labeltje op regel 2 en de knoppen op regel 3 staan daardoor op elke tegel op dezelfde plek, pal boven elkaar. Dat is de reden dat de knoppen op de onderste regel staan en er niet overheen zweven: een knop is 40 pixels hoog, en dat is anderhalve tekstregel - hij kwam dus altijd over dat labeltje heen. De tegel is daar een pixel of vijftien hoger van (ongeveer 126 in plaats van 110), en niets loopt meer over elkaar. Datums staan afgekort: *9 Jun. 2031*. De maandnamen staan met de hand in `Tonen` en komen niet uit de taalinstelling van het toestel; op een telefoon die op Engels staat las de lijst anders "9 June 2031" terwijl de rest van het scherm Nederlands is. Mijn kluis heeft bovenaan een keuzelijst om op soort te filteren, met de knop Toevoegen ernaast, net als op de website. Het filteren gebeurt bij de server (`KluisAsync` neemt een categorie mee) en niet in de lijst op het toestel: dan hoeft de telefoon niet alles op te halen om er drie dingen uit te laten zien. Bij Mijn kluis is bekijken en wijzigen hetzelfde scherm. Er was eerder ook een uitklap onder de tegel die precies dezelfde gegevens en bijlagen toonde: twee plekken om te onderhouden en twee manieren om hetzelfde te doen. ## Het logo Het logo staat op vier plekken, allemaal vanuit hetzelfde bestand: | Waar | Bestand | |---|---| | Inlogscherm van de app, groot | `src/MijnBesluit.AppUi/wwwroot/logo.png` | | Merkteken in de balk van de app, klein | hetzelfde bestand | | Kop en inlogpagina van de website | `src/MijnBesluit.Web/wwwroot/logo.png` | | Pictogram en startscherm van de app | `Resources/AppIcon/appicon.png`, `Resources/Splash/splash.png` | Het pictogram is **alleen het witte woordmerk op een doorzichtige achtergrond**, met het logogroen eronder via `Color="#C1D13F"` in het csproj. Zo maakt Android er zelf een rond, vierkant of druppelvormig pictogram van. Het woordmerk staat op zeventig procent van het vlak: alles buiten die cirkel mag een toestel wegsnijden, en op die maat blijft het hele woordmerk staan, hoe een telefoon het ook maskeert. Op 26 pixels - het merkteken in de balk - is het woordmerk niet te lezen. Dat hoeft ook niet: daar staat de naam in letters ernaast. Groot en leesbaar staat het op het inlogscherm. Nog niet gedaan: de melding op de telefoon gebruikt het app-pictogram als klein teken, en Android maakt daar een silhouet van. Een woordmerk wordt op 24 pixels een vlek. Een eigen, eenvoudig tekentje in `Platforms/Android/Resources/drawable` zou dat oplossen. ## Wat er nog niet in zit - Meldingen op de telefoon als er een bericht bezorgd wordt — de SocketServer kan uit zichzelf een melding sturen, maar een push-melding als de app dicht is vraagt om Firebase - iOS: `net10.0-ios` toevoegen aan `TargetFrameworks` en een Mac om op te bouwen - Een grens aan hoe lang de app op slot mag blijven staan. Nu blijft de sessie liggen tot iemand hem opent of uitlogt; na een maand ongebruikt zou hij zichzelf kunnen opruimen. ## Naar de Google Play Store Stand op 16 september 2026. De eisen van Google wijzigen; kijk ze na op . **Wat al klopt:** de app bouwt op `targetSdkVersion 36`, en dat is sinds 31 augustus 2026 de eis voor nieuwe apps. De rechten zijn zuinig (internet, camera, media, vingerafdruk, meldingen) en de meldingstoestemming wordt op het moment zelf gevraagd. **Wat er is gebouwd omdat Google het eist:** * **Account opheffen**, in de app én op `/account-verwijderen` zonder app. Zie `docs/Beveiliging.md`. * **Een privacyverklaring** op `/privacy`, met een link erheen vanaf het inlogscherm en vanuit Instellingen in de app. **Wat er nog moet:** 1. **Een organisatie-account** in de Play Console, op naam van het bedrijf - geen persoonlijk account. Persoonlijke accounts van na 13 november 2023 moeten eerst een gesloten test draaien met 12 testers die 14 dagen onafgebroken meedoen; organisatie-accounts niet. Een organisatie wordt wel geverifieerd (KvK, D-U-N-S), en dat duurt weken. Begin daarmee. 2. **Een AAB in plaats van een APK:** `dotnet publish -f net10.0-android -c Release -p:AndroidPackageFormat=aab` 3. **Een upload-keystore**, bewaard zoals de masterkey: apart, met back-up, buiten de projectmap. Zet Play App Signing aan; dan kan Google een nieuwe upload-sleutel geven als hij ooit weg is. Let op dat de app die gebruikers krijgen door Google opnieuw wordt ondertekend - voor de pushmeldingen maakt dat niets uit, maar alles wat aan een certificaatvingerafdruk hangt moet je met de sleutel van Google registreren. 4. **`ApplicationVersion` omhoog bij elke upload.** Staat nu op 1. 5. **Het Data safety-formulier**, de leeftijdsclassificatie en de winkelvermelding: icoon 512x512, banner 1024x500, minimaal twee schermafbeeldingen, korte en lange beschrijving, en het adres van de privacyverklaring. 6. **"Bevat advertenties" aanvinken** zodra de advertentiebalk meedraait. De inhoud van die advertenties valt dan onder het beleid van Google; dat is een reden te meer om ze vooraf zelf goed te keuren.