diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png index fd5a727..30ef6eb 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png index b044526..6968bcb 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png index 45deb09..09df4de 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png index a465558..13d7a24 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png index 5e2aad4..ac2a0ee 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/assets/icons/play_store_512.png b/assets/icons/play_store_512.png new file mode 100644 index 0000000..a122df1 Binary files /dev/null and b/assets/icons/play_store_512.png differ diff --git a/flutter_launcher_icons.yaml b/flutter_launcher_icons.yaml index 7c107b3..735854e 100644 --- a/flutter_launcher_icons.yaml +++ b/flutter_launcher_icons.yaml @@ -1,6 +1,6 @@ # flutter pub run flutter_launcher_icons flutter_launcher_icons: - image_path: "assets/icons/kooltap_logo.png" + image_path: "assets/icons/play_store_512.png" android: "launcher_icon" # image_path_android: "assets/icons/kooltap_logo.png" @@ -26,7 +26,7 @@ flutter_launcher_icons: windows: generate: true - image_path: "assets/icons/kooltap_logo.png" + image_path: "assets/icons/play_store_512.png" icon_size: 48 # min:48, max:256, default: 48 macos: diff --git a/lib/models/remote_command.dart b/lib/models/remote_command.dart index 57f0646..3967d96 100644 --- a/lib/models/remote_command.dart +++ b/lib/models/remote_command.dart @@ -85,12 +85,32 @@ class SyncResponse { } } +/// One line on a tab as shown in the dashboard, e.g. "10× Cola". +class SnapshotTabItem { + final String productName; + final int quantity; + final int unitPriceInCents; + + const SnapshotTabItem({ + required this.productName, + required this.quantity, + required this.unitPriceInCents, + }); + + Map toJson() => { + 'productName': productName, + 'quantity': quantity, + 'unitPriceInCents': unitPriceInCents, + }; +} + class SnapshotTab { final String id; final String customerName; final int version; final int totalInCents; final int itemCount; + final List items; const SnapshotTab({ required this.id, @@ -98,6 +118,7 @@ class SnapshotTab { required this.version, required this.totalInCents, required this.itemCount, + this.items = const [], }); Map toJson() => { @@ -106,6 +127,7 @@ class SnapshotTab { 'version': version, 'totalInCents': totalInCents, 'itemCount': itemCount, + 'items': items.map((item) => item.toJson()).toList(), }; } diff --git a/lib/services/remote_sync_service.dart b/lib/services/remote_sync_service.dart index dbf0717..8192bee 100644 --- a/lib/services/remote_sync_service.dart +++ b/lib/services/remote_sync_service.dart @@ -239,6 +239,14 @@ class RemoteSyncService { version: tab.version, totalInCents: tab.totalInCents, itemCount: tab.itemCount, + items: [ + for (final item in tab.items) + SnapshotTabItem( + productName: item.productName, + quantity: item.quantity, + unitPriceInCents: item.unitPriceInCents, + ), + ], ); } diff --git a/make_release.bat b/make_release.bat index 7e4fd03..74492ce 100644 --- a/make_release.bat +++ b/make_release.bat @@ -1,52 +1,3 @@ @echo off -setlocal EnableExtensions DisableDelayedExpansion - -cd /d "%~dp0" - -echo. -powershell -NoProfile -ExecutionPolicy Bypass -Command ^ - "$content = [IO.File]::ReadAllText('pubspec.yaml'); " ^ - "$match = [regex]::Match($content, '(?m)(?:\A|\r?\n)version:[ \t]*([^\r\n]*)'); " ^ - "if (-not $match.Success) { Write-Error 'Could not find the version line in pubspec.yaml.'; exit 1 }; " ^ - "Write-Output ('Current version: ' + $match.Groups[1].Value.Trim())" - -if errorlevel 1 ( - echo Could not read the current version. Release cancelled. - exit /b 1 -) - -set "APP_VERSION=" -set /p "APP_VERSION=Enter release version (for example 1.0.10 or 1.0.10+2): " - -if not defined APP_VERSION ( - echo No version entered. Release cancelled. - exit /b 1 -) - -powershell -NoProfile -ExecutionPolicy Bypass -Command ^ - "$version = $env:APP_VERSION; " ^ - "if ($version -notmatch '\A[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?\z') { " ^ - " Write-Error 'Use a version like 1.0.10 or 1.0.10+2.'; exit 1 " ^ - "}; " ^ - "$path = 'pubspec.yaml'; " ^ - "$content = [IO.File]::ReadAllText($path); " ^ - "$updated = ([regex]::new('(?m)(\A|\r?\n)version:[^\r\n]*')).Replace($content, ('${1}version: ' + $version), 1); " ^ - "if ($updated -eq $content) { Write-Error 'Could not find the version line in pubspec.yaml.'; exit 1 }; " ^ - "[IO.File]::WriteAllText($path, $updated)" - -if errorlevel 1 ( - echo Version update failed. Release cancelled. - exit /b 1 -) - -echo Updated pubspec.yaml to version %APP_VERSION%. -echo Building release APK... -flutter build apk --release - -if errorlevel 1 ( - echo Release APK build failed. - exit /b 1 -) - -echo Release APK created at build\app\outputs\flutter-apk\app-release.apk -endlocal +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0make_release.ps1" %* +exit /b %errorlevel% diff --git a/make_release.ps1 b/make_release.ps1 new file mode 100644 index 0000000..3f344d4 --- /dev/null +++ b/make_release.ps1 @@ -0,0 +1,117 @@ +param( + [switch]$SkipTests, + [switch]$SkipUpload +) + +$ErrorActionPreference = 'Stop' +Set-Location $PSScriptRoot + +$updateServer = 'https://updater.kooltap.com' +$apkPath = 'build\app\outputs\flutter-apk\app-release.apk' + +function Fail([string]$message) { + Write-Host $message -ForegroundColor Red + exit 1 +} + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$pubspec = [IO.File]::ReadAllText('pubspec.yaml') +$match = [regex]::Match( + $pubspec, + '(?m)^version:[ \t]*([0-9]+\.[0-9]+\.[0-9]+)\+([0-9]+)' +) +if (-not $match.Success) { + Fail 'Could not find a version like 1.2.3+4 in pubspec.yaml.' +} + +$currentName = $match.Groups[1].Value +$currentBuild = [int]$match.Groups[2].Value +Write-Host "Current version: $currentName+$currentBuild" + +# The server picks the latest release by build number, so every release needs +# a higher one than anything already published +$publishedName = $null +$publishedBuild = 0 +try { + foreach ($release in Invoke-RestMethod "$updateServer/api/releases") { + if ($release.VersionCode -gt $publishedBuild) { + $publishedName = $release.Version + $publishedBuild = [int]$release.VersionCode + } + } + if ($publishedName) { + Write-Host "Latest published: $publishedName+$publishedBuild" + } +} catch { + Write-Host "Could not reach $updateServer, using pubspec.yaml only." -ForegroundColor Yellow +} + +$name = Read-Host 'Release version (for example 1.2.2)' +if ($name -notmatch '\A[0-9]+\.[0-9]+\.[0-9]+\z') { + Fail 'Use a version like 1.2.2.' +} +if ($publishedName -and [version]$name -le [version]$publishedName) { + Fail "$name must be higher than the published $publishedName, or tablets will not see it." +} + +$build = [Math]::Max($currentBuild, $publishedBuild) + 1 +$version = "$name+$build" + +$versionStart = $match.Groups[1].Index +$versionEnd = $match.Groups[2].Index + $match.Groups[2].Length +$pubspec = $pubspec.Substring(0, $versionStart) + $version + $pubspec.Substring($versionEnd) +[IO.File]::WriteAllText((Resolve-Path 'pubspec.yaml'), $pubspec) +Write-Host "Updated pubspec.yaml to version $version." + +if (-not $SkipTests) { + Write-Host 'Running tests...' + flutter test + if ($LASTEXITCODE -ne 0) { Fail 'Tests failed. Release cancelled.' } +} + +Write-Host 'Building release APK...' +flutter build apk --release +if ($LASTEXITCODE -ne 0) { Fail 'Release APK build failed.' } +Write-Host "Release APK created at $apkPath" + +if ($SkipUpload) { exit 0 } + +$token = $env:KOOLTAP_ADMIN_TOKEN +if (-not $token) { + Write-Host 'KOOLTAP_ADMIN_TOKEN is not set, skipping the upload.' -ForegroundColor Yellow + exit 0 +} + +$notes = Read-Host 'Release notes' +$mandatory = if ((Read-Host 'Mandatory update? (y/N)') -match '\A[yY]') { 'true' } else { 'false' } + +Write-Host "Uploading $version to $updateServer..." + +Add-Type -AssemblyName System.Net.Http +$client = [Net.Http.HttpClient]::new() +$client.Timeout = [TimeSpan]::FromMinutes(5) +$client.DefaultRequestHeaders.Add('X-Admin-Token', $token) + +$apk = [IO.File]::OpenRead((Resolve-Path $apkPath)) +$form = [Net.Http.MultipartFormDataContent]::new() +$form.Add([Net.Http.StreamContent]::new($apk), 'apk', 'app-release.apk') +$form.Add([Net.Http.StringContent]::new($notes), 'notes') +$form.Add([Net.Http.StringContent]::new($mandatory), 'mandatory') + +try { + $response = $client.PostAsync("$updateServer/admin/releases", $form).GetAwaiter().GetResult() + $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() +} finally { + $form.Dispose() + $apk.Dispose() + $client.Dispose() +} + +if (-not $response.IsSuccessStatusCode) { + Fail "Upload failed (HTTP $([int]$response.StatusCode)): $body`nThe APK is still at $apkPath" +} + +$release = $body | ConvertFrom-Json +Write-Host "Published $($release.Version) (build $($release.VersionCode)), sha256 $($release.SHA256)" -ForegroundColor Green +Write-Host "Don't forget to commit pubspec.yaml and tag the release: git tag v$version" diff --git a/pubspec.yaml b/pubspec.yaml index 52baf87..2e4c11c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: kooltap description: "KoolTap: a tablet point-of-sale app for running bar tabs." publish_to: 'none' -version: 1.1.0 +version: 1.2.1+1 environment: sdk: ^3.12.2 diff --git a/test/remote_sync_service_test.dart b/test/remote_sync_service_test.dart index 674cbc5..1fe0dcd 100644 --- a/test/remote_sync_service_test.dart +++ b/test/remote_sync_service_test.dart @@ -136,6 +136,9 @@ void main() { final snapshot = server.syncRequests.single['openTabs'] as List; expect(snapshot.single['customerName'], 'Alice'); expect(snapshot.single['totalInCents'], 400); + expect(snapshot.single['items'], [ + {'productName': 'Beer', 'quantity': 1, 'unitPriceInCents': 400}, + ]); expect(eventTypes(), [RemoteSyncEventType.syncCompleted]); expect(await service.lastSuccessfulSyncAt, isNotNull); diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index 795678b..28631a8 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ