<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[OrionApps Engineering]]></title><description><![CDATA[OrionApps Engineering]]></description><link>https://orionapps.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 14 Nov 2026 20:29:26 GMT</lastBuildDate><atom:link href="https://orionapps.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building an offline-first POS app for Android]]></title><description><![CDATA[How we built OneTill, a free offline POS app for Android: integer money maths, a printer that ignored text commands, and a billing bug.]]></description><link>https://orionapps.hashnode.dev/building-an-offline-first-pos-app-for-android</link><guid isPermaLink="true">https://orionapps.hashnode.dev/building-an-offline-first-pos-app-for-android</guid><category><![CDATA[Android]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[indie-hacker]]></category><dc:creator><![CDATA[Orion Support Team]]></dc:creator><pubDate>Thu, 12 Nov 2026 19:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6af704d7ea9b32b27a067851/4d929a67-8ed9-4a81-8002-2f90956c4071.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>OneTill is a free point-of-sale and inventory app for Android, made for shops where the owner is also the cashier. It runs entirely on the phone, works without internet, and needs no account. It is written in Kotlin with Jetpack Compose, Room, CameraX and ML Kit.</p>
<p>This post covers the decisions that shaped it and the three problems that cost the most time: money maths, a printer that ignored every text command, and a billing bug that looked like someone else's fault.</p>
<h2>Start with the shopkeeper</h2>
<p>The first design question was who stands behind the counter. For a corner shop or a market stall it is one person, often with a customer waiting and one hand full. That ruled out a few things early.</p>
<p>There is no OneTill account and no server. The app asks for a PIN and keeps everything in a local database. We looked at syncing between two phones and dropped it for the first release: merging stock counts needs per-row IDs, a change log and conflict rules, and a wrong merge means a wrong count on the shelf. One phone, one source of truth.</p>
<h2>Money is integers</h2>
<p>Prices are stored as whole cents, so $12.50 is stored as 1250. Quantities are stored in thousandths, so a half kilo of cheese is 500. Tax rates are stored in basis points, so 8.25% is 825.</p>
<p>We did this because floating point and refunds do not mix. A partial return has to reverse the right share of the discount and the tax, and the rounding has to land on exactly the same total the customer paid. With integers that becomes ordinary arithmetic you can unit test, and we do.</p>
<p>Every action that touches money is one Room transaction. A sale writes the sale, its lines and the stock movements together. If anything fails, nothing is saved, and the shelf count never disagrees with the till.</p>
<h2>Scanning</h2>
<p>Barcode scanning uses CameraX with ML Kit. The scanner is full screen with an aiming frame, a torch button and a running count and total, so you can scan a whole basket and then tap Done to return to the order. A code that stays in view is added once, not once per frame. It also accepts keyboard-style scanners.</p>
<h2>The printer that printed nothing</h2>
<p>Receipt printing was meant to be the easy part. The standard approach for cheap 58 mm thermal printers is ESC/POS: you send text and a few control codes. The printer on our test bench, a USB model, ignored all of it.</p>
<p>It turned out to be a label printer that speaks TSPL and has no built-in fonts at all. There was nothing for text commands to select. The fix was to render the receipt as a bitmap, 384 dots wide (58 mm at 8 dots per millimeter), and send the image. We got the exact command bytes by capturing what the Windows driver sent when it printed a test label.</p>
<p>ESC/POS stays for normal receipt printers, and the setting is called printer type: ESC/POS or picture. If you are debugging a cheap printer that prints blanks, check whether it is a label printer before you blame your commands.</p>
<h2>Backup without accounts</h2>
<p>With no OneTill account there is no cloud copy of your data, so backup had to be simple and the shopkeeper's own. You can export a file (.otb, with an optional passphrase) and restore it on another phone. Google Drive backup goes into the app's private folder in your own Drive, keeps the latest five, and runs daily for Pro users. If someone forgets their PIN, the phone's screen lock is the recovery route.</p>
<h2>A billing bug that looked like Google's</h2>
<p>OneTill Pro is one subscription that removes ads and adds a few extras. For days the Pro screen showed "Plans unavailable" and we assumed Play was slow. That was partly true. New subscriptions can take a while to show up, and our testing track had been paused with a change waiting for review, which made Play return "Item not found".</p>
<p>But there was also a bug of ours underneath. We had enabled automatic service reconnection in the billing client, which makes the client report itself as ready at startup. Our start function checked that flag and returned early, so the product query never ran. The fix was to load products whenever the client is ready and to refresh them when the Pro screen opens. We found it by logging the result of the product query, and we should have added that log on day one.</p>
<h2>Testing without losing a shop's data</h2>
<p>Room schema changes need a migration, and the only reliable test is installing the new version over data from the old one. We have migration tests for every version and an on-device suite of about 100 tests. The suite clears app data, so it runs on emulators only, never on a phone that holds real records.</p>
<h2>What is next</h2>
<p>Bluetooth printing is not built yet. Most of the remaining work is polish from real use: fewer taps on the sell screen and clearer reports.</p>
<p>OneTill is free, with ads. An optional Pro subscription removes them.</p>
<ul>
<li><a href="https://play.google.com/store/apps/details?id=com.orionapps.onetill">OneTill on Google Play</a></li>
<li><a href="https://orionapps.com/onetill/">Details and screenshots</a></li>
</ul>
<p>If you build for small shops, or you have fixed a printer that would not print, we would like to hear how you did it.<br /><br />OneTill is also on <a href="https://www.producthunt.com/products/onetill">Product Hunt</a> where we'd welcome feedback</p>
]]></content:encoded></item></channel></rss>