Skip to main content

Changelog

This document records notable code changes and release deltas for the Qprint platform.


February 2026 — Page count by file type, convert-to-PDF message, pre-convert cache

Backend: real page/slide count and "convert to PDF" when unavailable

Summary: Billing uses real page or slide count for all supported office formats. When the backend cannot determine page/slide count, the API returns 400 with a clear message asking the user to convert to PDF and upload.

AreaChange
BackendNew backend/internal/utils/office.go: CountPPTXSlides (ZIP slide XMLs), CountDOCXPages (app.xml or document.xml breaks), CountDOCPages (OLE SummaryInformation "Page Count"), CountPPTSlides (binary .ppt via mscfb + PersistDirectoryAtom). Dependency: github.com/richardlehane/mscfb, msoleps.
Handlersfiles.go and payment.go: PDF uses existing count; DOCX/PPTX/DOC/PPT use the new count functions. ErrPageCountNotAvailable and PageCountNotAvailableMessage ("Page count not available for this file. Please convert to PDF and upload."). When any count function returns this error, API responds with 400 and that message (single-file upload, batch upload, calculate-cost).
DOCXIf app.xml has no <Pages> and document.xml is missing or unreadable → ErrPageCountNotAvailable.
DOCIf OLE SummaryInformation has no "Page Count" or invalid → ErrPageCountNotAvailable.
PPTXIf no slides found (count 0) → ErrPageCountNotAvailable.
PPTIf binary structure cannot be parsed (wrong records, no persist dir, etc.) → ErrPageCountNotAvailable.

Files: backend/internal/utils/office.go, backend/internal/handlers/files.go, backend/internal/handlers/payment.go, backend/go.mod.

Shopkeeper app: pre-convert cache and LibreOffice cmd.exe

Summary: Queue files are pre-converted (download + LibreOffice→PDF) in the background when the queue loads. At print time the app uses the cached PDF if ready, otherwise waits (up to 90s) or falls back to on-demand convert. LibreOffice on Windows is run via cmd.exe with cd /d outdir && soffice ... so the output PDF is written to the correct folder.

AreaChange
Pre-convertlib/services/preconvert_cache.dart: per-file state (idle → downloading → converting → ready / failed). When queue list changes, app calls startPreconvert(fileId, ...) for each file (download then ConverterEngine.ensurePdf). At print: getReadyPdfPath then waitForReady(90s) then on-demand; after successful print deleteLocalFiles and remove.
Converterlib/services/converter_engine.dart: on Windows, LibreOffice is invoked via C:\Windows\System32\cmd.exe /c "cd /d outdir && soffice ..." so the PDF is created in the intended directory (fixes portable LibreOffice writing to wrong path).
DashboardAfter queue load, _triggerPreconvertForQueue starts preconvert for each file in the queue. Batch print uses cache first, then wait/on-demand; after print, temp files for that file are cleaned up.

Files: shopkeeper_app/lib/services/preconvert_cache.dart, shopkeeper_app/lib/services/converter_engine.dart, shopkeeper_app/lib/screens/dashboard_screen.dart.


February 2026 — Shopkeeper cancel order with reason

Shopkeeper can cancel queue order (before printing)

Summary: When something is wrong (e.g. printer out of order, paper not available), the shopkeeper can cancel a queue order with a required reason. The customer is notified, sees the reason, order status becomes cancelled, and the customer receives a refund. The shopkeeper cannot cancel once printing has started.

AreaChange
BackendNew file status: cancelled. New columns on files: cancel_reason (TEXT), cancelled_at (TIMESTAMP), cancelled_by (INT FK to users). POST /queue/:fileId/cancel with body { "reason": "..." }: shopkeeper-only; cancels the whole order (all files with same payment_order_id at this shop); allowed only when all files are status uploaded (returns 400 if any file is printing). If the order was paid, refund is processed once per payment order with reason "Order cancelled by shop: <reason>". FCM notification sent to customer with the reason. Cancelled orders excluded from GET /queue and from order-files.
GetMyOrders / GetMyFilesResponse includes any_cancelled and cancel_reason; derived status order is cancelled > withdrawn > downloaded > printing > uploaded.
GetShopQueueResponse includes first_file_id per order (for cancel/print-started); excludes cancelled from queue.
Customer Android appStatus cancelled shown as "❌ Cancelled by shop" (orange); when order['cancel_reason'] is set, a reason box is shown. Withdraw and pending cost logic exclude cancelled.
Shopkeeper Windows appCancel button next to Print for queue items in idle state; dialog with required reason (max 500 chars); on confirm calls POST /queue/:fileId/cancel, refreshes queue, shows error on 400 (e.g. already printing).
Frontend (web)Customer dashboard: Status "cancelled" with orange badge and cancel_reason; withdraw hidden for cancelled; "Pending costs" exclude cancelled. Shopkeeper dashboard: "Cancel order" button per queue row; modal with required reason; calls cancelQueueOrder(fileId, reason); success refreshes queue, errors shown in modal. API: cancelQueueOrder(fileId, reason) in lib/api.ts.

Edge cases: Cannot cancel if any file in the order is printing. Only uploaded files at the shop are cancelled. Refund is once per payment order. Customer is notified by FCM with the reason.

Files: backend/internal/database/schema.go, backend/internal/handlers/files.go, backend/cmd/api/main.go; customer_app/lib/screens/files_screen.dart, customer_app/lib/theme/app_colors.dart, customer_app/lib/screens/expenses_screen.dart; shopkeeper_app/lib/models/print_job.dart, shopkeeper_app/lib/services/api_service.dart, shopkeeper_app/lib/screens/dashboard_screen.dart; frontend/lib/api.ts, frontend/app/customer/dashboard/page.tsx, frontend/app/shopkeeper/dashboard/page.tsx.


February 2026 — Windows installer, customer app referral & Android review

Shopkeeper app: Windows installer (MSIX + classic)

Summary: The shopkeeper app now has a full installer workflow: MSIX package (Publisher: Qprint, white logo background, desktop shortcut) and an optional classic .exe installer with install-directory choice and desktop-shortcut checkbox.

AreaChange
MSIXbuild_installer.bat runs dart run msix:create, then scripts\patch_and_repack_msix.ps1 to set white tile/installer logo background, add desktop shortcut ("Qprint Shop"), and replace Store logo assets with white-background versions. Installer shows Publisher: Qprint (no longer "Msix Testing") via signing cert.
Signing certscripts\create_signing_cert.ps1 creates windows\Qprint.pfx (CN=Qprint). pubspec.yaml msix_config uses certificate_path, certificate_password, publisher: CN=Qprint. Cert is created automatically if missing when running build_installer.bat.
Classic installerinstaller_script.iss (Inno Setup): default install dir {autopf}\Qprint Shop (Program Files), user can change; "Create a desktop shortcut" task (checkbox). Output: build\windows\installer\QprintShop_Setup.exe. Build with iscc installer_script.iss after flutter build windows.
Patch scriptscripts\patch_and_repack_msix.ps1 finds .msix in build\windows\runner\Release or build\windows\x64\runner\Release, unpacks with MakeAppx, patches manifest (BackgroundColor #FFFFFF, desktop7:Shortcut), replaces Assets logo PNGs with white-background versions, repacks.

Files: shopkeeper_app/build_installer.bat, shopkeeper_app/scripts/patch_and_repack_msix.ps1, shopkeeper_app/scripts/create_signing_cert.ps1, shopkeeper_app/installer_script.iss, shopkeeper_app/pubspec.yaml (msix_config), shopkeeper_app/WINDOWS_INSTALLER.md, shopkeeper_app/.gitignore (windows/Qprint.pfx).


Customer app: referral code sanitization & safe error

Summary: Referral codes from deep links or user input are sanitized before sending to the API. Safe-error handling includes a dedicated context for referral screens.

AreaChange
Referral sanitizerlib/utils/referral_sanitizer.dart: sanitizeReferralCode(String?) returns only alphanumeric, underscore, hyphen; max 64 chars. Used in api_service.dart for POST /register and in main.dart for deep-link referral.
Safe errorlib/utils/safe_error.dart: context 'referral' returns a user-friendly message for referral/profile load failures ("Could not load your referral code. Pull down to refresh or tap Retry.").
Registrationapi_service.register() sends referral_code: sanitizeReferralCode(referralCode) so invalid characters from links or paste are stripped.

Files: customer_app/lib/utils/referral_sanitizer.dart, customer_app/lib/utils/safe_error.dart, customer_app/lib/services/api_service.dart, customer_app/lib/main.dart, customer_app/lib/screens/register_screen.dart, customer_app/lib/screens/refer_and_earn_screen.dart.


Customer app: Android review and optimizations

Summary: Full Android-focused review documented in customer_app/CUSTOMER_APP_ANDROID_REVIEW.md: build config (compileSdk 36, targetSdk 35), map/dashboard/upload edge cases, and recommendations.

AreaChange
Map screenMarkers updated via new Set<Marker> in setState; load failure clears shops and shows orange banner "Could not load shops. Tap refresh to retry."
Upload screenFile name display uses file.path.split(RegExp(r'[/\\]')) for correct name on Android and Windows.
DashboardAvatar shows ? when _username is empty instead of throwing on _username[0].

Reference: customer_app/CUSTOMER_APP_ANDROID_REVIEW.md (in repo; not duplicated in project-docs).


February 2026 — Print flow, withdraw rules & security

Summary: When the shopkeeper starts a print, the customer can no longer withdraw until the print either completes (confirmed) or fails. This prevents loss when the shop has already committed to printing.

AreaChange
BackendNew file status: printing. New endpoints: POST /queue/:fileId/print-started, POST /queue/:fileId/print-failed, POST /file/:code/print-started, POST /file/:code/print-failed. GET /my-orders returns status printing when any file in the order is printing. POST /withdraw/:fileId returns 409 when status is printing. Confirm endpoints accept status uploaded or printing.
Shopkeeper appCalls print-started when starting a print (queue and private); calls print-failed on failure or verification timeout. Verification timeout is page-based: 60 + (15 × num_pages) seconds (no cap).
Customer app & webWithdraw button hidden when order status is printing; status shows "Printing at shop...".

See PRINT_AND_WITHDRAW_FINAL_DESIGN.md in the project root for the full design.


Summary: Setting SKIP_AUTH_COOKIE=true stops the backend from setting an auth cookie on login. Only requests that send Authorization: Bearer <token> (from the app or frontend) are then authenticated. Typing the API base URL (e.g. /shops) in the browser in another tab no longer sends credentials, so the API returns 401 and no data.

LocationChange
backend/internal/handlers/auth.goskipAuthCookie() reads SKIP_AUTH_COOKIE; when true, setAuthCookie and clearAuthCookie are no-ops. Token is still returned in the login response body for Bearer use.

Shopkeeper delete and customer prints

Summary: Documented behavior (no code change): When an admin deletes a shopkeeper, customer orders/files are unlinked (shop_id/shopkeeper_id set to NULL); customers can still withdraw and get a refund. When a shopkeeper self-deletes, the delete fails if there are related prints/orders (foreign key). There is no explicit "shop deleted" status shown to the customer; the order simply has no shop name until they withdraw. See SHOPKEEPER_DELETE_AND_CUSTOMER_PRINTS_ANALYSIS.md in the project root.


Customer app version

LocationChange
customer_app/pubspec.yamlversion: 1.0.5+81.0.6+9 (versionName 1.0.6, versionCode 9) for new app bundle.

February 2026 — Customer app & backend

Session idle timeout (15 days)

Summary: Customer app auto-logout after inactivity was changed from 30 minutes to 15 days.

LocationChange
customer_app/lib/services/session_service.dartidleTimeout = Duration(minutes: 30)Duration(days: 15)
customer_app/SECURITY_SETUP.mdSession timeout description and summary table: "30 min" → "15 days"

Rationale: Reduces friction for occasional users while still clearing session after extended inactivity.


Registration: specific duplicate-field messages

Summary: When registration fails because username, email, or mobile number is already in use, the user now sees which field(s) are taken (e.g. "This username is already in use." or "This email and mobile number are already in use.").

Backend

LocationChange
backend/internal/handlers/auth.goReplaced single duplicate check with three separate checks (username, email, phone). Builds a user-facing message listing which of "username", "email", or "mobile number" are already in use. Returns 400 with that message (one field: "This X is already in use."; multiple: "This X and Y are already in use." / "This X, Y and Z are already in use.").

Customer app

LocationChange
customer_app/lib/services/api_service.dartOn register 400/409, if response body contains "already exists" or "already in use", throw Exception(body) so the backend message is shown.
customer_app/lib/utils/safe_error.dartAllow through messages containing "already in use" and (username or email or mobile) so they are not replaced by a generic "Registration failed" message.

Android build: SDK 36 and app version bump

Summary: Customer app Android build updated for Google Play 2025 (target API 35) and plugin compatibility (compileSdk 36). App version bumped for new release.

LocationChange
customer_app/android/app/build.gradle.ktscompileSdk = 35compileSdk = 36 (required by flutter_secure_storage, google_maps_flutter_android, etc.). minSdk = flutter.minSdkVersionminSdk = 21. targetSdk = flutter.targetSdkVersiontargetSdk = 35 (Play Store requirement).
customer_app/pubspec.yamlversion: 1.0.4+7version: 1.0.5+8 (versionName 1.0.5, versionCode 8 for Play Store upload).

Note: compileSdk is what the app compiles against (backward compatible); targetSdk 35 satisfies Play Store. Plugins and AndroidX (e.g. androidx.browser:browser, androidx.core:core-ktx) require compileSdk 36.


Reference: files touched (February 2026 delta)

ComponentFiles
Backendbackend/internal/handlers/auth.go, backend/internal/handlers/files.go, backend/internal/database/schema.go, backend/cmd/api/main.go
Customer appcustomer_app/lib/screens/files_screen.dart, customer_app/lib/theme/app_colors.dart, customer_app/lib/screens/expenses_screen.dart, customer_app/pubspec.yaml, lib/utils/safe_error.dart, lib/utils/referral_sanitizer.dart, lib/services/api_service.dart, lib/main.dart, lib/screens/register_screen.dart, lib/screens/refer_and_earn_screen.dart, dashboard, upload, map_screen, build.gradle.kts, SECURITY_SETUP.md, CUSTOMER_APP_ANDROID_REVIEW.md
Shopkeeper appshopkeeper_app/lib/models/print_job.dart, shopkeeper_app/lib/services/api_service.dart, shopkeeper_app/lib/screens/dashboard_screen.dart; installer: build_installer.bat, scripts/patch_and_repack_msix.ps1, scripts/create_signing_cert.ps1, installer_script.iss, pubspec.yaml (msix_config), WINDOWS_INSTALLER.md
Frontendfrontend/lib/api.ts, frontend/app/customer/dashboard/page.tsx, frontend/app/shopkeeper/dashboard/page.tsx

Earlier updates (summary)

  • Jan 31, 2026 — Customer app: Secure Storage, certificate pinning, root detection, session timeout (30 min). CSRF Bearer-only exemption for API clients.
  • Feb 2026 — Shopkeeper app: Certificate pinning, HTTP timeout, API URL config, PowerShell injection fix, path traversal, session timeout (30 min idle), token fallback restriction, file integrity verification.

For full security implementation lists, see Customer App Security and Shopkeeper App Audit.


Last Updated: February 2026 (Page count by file type, convert-to-PDF message, pre-convert cache; shopkeeper cancel with reason; Windows installer; referral & Android review)