Skip to main content

How to Make a POS App

Copied to clipboard!
No boilerplate memorization required

How to Vibe Code a Native Android POS App

Stay in your creative flow. Direct AI models with high-level retail intent, stitch together a Jetpack Compose Point-of-Sale register with live stock tracking, promo discounts, and itemized receipts, and run it on your phone or tablet in under 30 minutes.

Core Stack

Kotlin Compose

Order Engine

Reactive Cart + Stock

AI Partner

Claude / Gemini

Target Time

20โ€“30 Mins

The Philosophy

The Vibe Coding Mobile Philosophy

Building a Point of Sale (POS) terminal traditionally meant weeks of wiring RecyclerViews, SQLite inventory triggers, tax rounding helpers, and payment state machines. Vibe coding flips the process: you define your catalog categories, tax/discount rules, and checkout flow, while conversational AI writes the reactive Compose state and receipt generator.

Old-School Mobile Stress

  • Manually syncing cart quantities with RecyclerView adapter positions.
  • Floating-point rounding bugs on tax, tips, and percentage discounts.
  • Writing brittle XML layouts that break between phones and 10-inch tablets.
  • Spending days mocking simple receipt previews and payment dialogs.

The Vibe Coder Way

  • Prompt AI for a reactive product grid with instant category filtering.
  • Get automatic subtotal, promo discount, and tax calculations in real time.
  • Deduct inventory stock counts automatically when an order is completed.
  • Generate itemized digital receipts ready for Bluetooth thermal printing.
Execution

The 4-Step Blueprint

From an empty Android Studio project to a fast cashier checkout register.

Step 01

Empty Compose Setup

Create a new Empty Activity (Compose) project in Android Studio named VibePOS. Material 3 provides grids, filter chips, and receipt modals out of the box.

๐Ÿ’ก Pro Vibe Tip: Jetpack Compose's LazyVerticalGrid adapts cleanly from compact handheld phones to wide countertop tablets.
Step 02

Configure Your Register

Choose your store vertical, inventory tracking level, and checkout workflow in the configurator below, then paste the prompt into Claude or Gemini.

๐Ÿ”‘ Key Constraint: Ask the AI to deduct cart quantities from product stock on checkout and disable "SOLD OUT" tiles automatically.
Step 03

Single-File Drop-In

Paste the generated code into MainActivity.kt to test category filtering, VIP discount math, tender selection, and itemized receipts.

โœจ Iteration Secret: Format the receipt dialog with FontFamily.Monospace so it mirrors a real 58mm/80mm thermal paper printout.
Step 04

Add Hardware & Persistence

Use the Prompt Vault below to add cash-tendered change calculation, stock management sheets, and end-of-shift Z-reports.

๐Ÿ† The Grand Payoff: A custom, zero-monthly-fee POS register running natively on your Android phone or countertop tablet.
Interactive Tool

Vibe Prompt Configurator

Select your store vertical, inventory engine, and checkout features. Copy the generated prompt directly into Claude, Cursor, or Gemini.

Special Feature Vibe
Optional custom details added straight into your master prompt.
Generated Master Prompt
Ready to paste into Claude, Gemini, or ChatGPT Single-file Compose ready
Product Spec

Essential Elements of a POS App

A Point of Sale (POS) app powers retail and hospitality checkout counters. Make sure your register includes these 6 core capabilities:

  • Accept Multiple Payment Methods

    Support credit and debit cards, NFC mobile wallets (Google Pay / Apple Pay), gift cards, and cash with instant change-due calculation.

  • Track Sales & Inventory Stock

    Monitor product stock levels in real time, show low-stock alerts on item tiles, and automatically decrement inventory when orders complete.

  • Generate Receipts & Promo Discounts

    Calculate percentage promos and sales tax accurately, and generate itemized digital or printed receipts for every transaction.

  • Manage Customer Loyalty Profiles

    Keep track of repeat customers, contact details, purchase history, and VIP loyalty discounts right at the checkout counter.

  • Connect Thermal Receipt Printers

    Format receipts in fixed-width monospace columns ready to send to wireless Wi-Fi or Bluetooth ESC/POS thermal receipt printers.

  • Real-Time Shift & Revenue Summary

    Give store owners an instant header readout of completed orders and gross shift revenue so they always know how the day is performing.

UX Checklist Before Shipping: Keep product tiles large for rapid cashier tapping, display live cart badges (x2, x3) directly on product cards, and format all currency values to two decimal places.
Starter Template

The "Golden" Single-File Compose Template

Replace the contents of your MainActivity.kt with this to run a working POS Register right now.

app/src/main/java/.../MainActivity.kt
Kotlin + Compose
package com.example.vibepos

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

data class PosProduct(
    val id: Int,
    val name: String,
    val category: String,
    val price: Double,
    val stock: Int
)

data class ReceiptRecord(
    val orderNumber: Int,
    val items: List<Pair<PosProduct, Int>>,
    val subtotal: Double,
    val discount: Double,
    val tax: Double,
    val total: Double,
    val paymentMethod: String
)

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                colorScheme = darkColorScheme(
                    primary = Color(0xFF10B981),
                    background = Color(0xFF0B1320),
                    surface = Color(0xFF162032)
                )
            ) {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    PosTerminalScreen()
                }
            }
        }
    }
}

@Composable
fun PosTerminalScreen() {
    val categories = listOf("All", "Coffee", "Bakery", "Merch")
    var selectedCategory by remember { mutableStateOf("All") }

    val catalog = remember {
        mutableStateListOf(
            PosProduct(1, "Oat Cortado", "Coffee", 4.75, 14),
            PosProduct(2, "Cold Brew Tonic", "Coffee", 5.50, 9),
            PosProduct(3, "Matcha Latte", "Coffee", 5.25, 11),
            PosProduct(4, "Almond Croissant", "Bakery", 4.50, 6),
            PosProduct(5, "Cardamom Bun", "Bakery", 4.25, 8),
            PosProduct(6, "Ceramic Tumbler", "Merch", 24.00, 4)
        )
    }

    val cart = remember { mutableStateMapOf<Int, Int>() }
    var applyVipDiscount by remember { mutableStateOf(false) }
    var paymentMethod by remember { mutableStateOf("Card Tap") }
    var completedOrders by remember { mutableIntStateOf(0) }
    var dailyRevenue by remember { mutableDoubleStateOf(0.0) }
    var activeReceipt by remember { mutableStateOf<ReceiptRecord?>(null) }

    val filteredProducts = catalog.filter {
        selectedCategory == "All" || it.category == selectedCategory
    }

    val cartEntries = cart.entries.mapNotNull { (id, qty) ->
        catalog.find { it.id == id }?.let { it to qty }
    }
    val subtotal = cartEntries.sumOf { (prod, qty) -> prod.price * qty }
    val discount = if (applyVipDiscount) subtotal * 0.10 else 0.0
    val taxable = (subtotal - discount).coerceAtLeast(0.0)
    val tax = taxable * 0.08
    val grandTotal = taxable + tax

    Column(modifier = Modifier.fillMaxSize().padding(14.dp)) {
        // Top Shift Summary Bar
        Row(
            modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Column {
                Text("VibePOS Register #01", fontSize = 20.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
                Text("Orders: $completedOrders โ€ข Shift Sales: $${String.format("%.2f", dailyRevenue)}", fontSize = 12.sp, color = Color(0xFF34D399))
            }
            FilterChip(
                selected = applyVipDiscount,
                onClick = { applyVipDiscount = !applyVipDiscount },
                label = { Text(if (applyVipDiscount) "VIP 10% ON" else "VIP Promo (10%)") }
            )
        }

        // Category Filter Pills
        LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(bottom = 10.dp)) {
            items(categories) { cat ->
                FilterChip(
                    selected = selectedCategory == cat,
                    onClick = { selectedCategory = cat },
                    label = { Text(cat) }
                )
            }
        }

        // Product Tile Grid
        LazyVerticalGrid(
            columns = GridCells.Fixed(2),
            verticalArrangement = Arrangement.spacedBy(10.dp),
            horizontalArrangement = Arrangement.spacedBy(10.dp),
            modifier = Modifier.weight(1f)
        ) {
            items(filteredProducts, key = { it.id }) { product ->
                val inCart = cart[product.id] ?: 0
                val remaining = product.stock - inCart
                Card(
                    shape = RoundedCornerShape(16.dp),
                    colors = CardDefaults.cardColors(
                        containerColor = if (remaining > 0) MaterialTheme.colorScheme.surface else Color(0xFF1E293B).copy(alpha = 0.5f)
                    ),
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable(enabled = remaining > 0) {
                            cart[product.id] = inCart + 1
                        }
                ) {
                    Column(modifier = Modifier.padding(12.dp)) {
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text(product.category.uppercase(), fontSize = 10.sp, color = Color(0xFF94A3B8), fontWeight = FontWeight.Bold)
                            Text(
                                text = if (remaining > 0) "$remaining left" else "SOLD OUT",
                                fontSize = 10.sp,
                                color = if (remaining > 3) Color(0xFF34D399) else Color(0xFFF87171),
                                fontWeight = FontWeight.Bold
                            )
                        }
                        Spacer(modifier = Modifier.height(6.dp))
                        Text(product.name, fontSize = 15.sp, fontWeight = FontWeight.Bold, color = Color.White)
                        Spacer(modifier = Modifier.height(4.dp))
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
                            Text("$${String.format("%.2f", product.price)}", fontSize = 14.sp, color = Color(0xFF38BDF8), fontWeight = FontWeight.SemiBold)
                            if (inCart > 0) {
                                Box(
                                    modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(Color(0xFF10B981)).padding(horizontal = 8.dp, vertical = 2.dp)
                                ) {
                                    Text("x$inCart", fontSize = 11.sp, color = Color.Black, fontWeight = FontWeight.ExtraBold)
                                }
                            }
                        }
                    }
                }
            }
        }

        Spacer(modifier = Modifier.height(10.dp))

        // Order Ticket & Checkout Dock
        Card(
            shape = RoundedCornerShape(18.dp),
            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
        ) {
            Column(modifier = Modifier.padding(14.dp)) {
                Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
                    Text("Current Ticket (${cartEntries.sumOf { it.second }} items)", fontWeight = FontWeight.Bold, color = Color.White, fontSize = 14.sp)
                    if (cart.isNotEmpty()) {
                        TextButton(onClick = { cart.clear() }) {
                            Text("Clear Ticket", fontSize = 12.sp, color = Color(0xFFF87171))
                        }
                    }
                }

                if (cartEntries.isNotEmpty()) {
                    LazyColumn(modifier = Modifier.heightIn(max = 96.dp)) {
                        items(cartEntries) { (prod, qty) ->
                            Row(
                                modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
                                horizontalArrangement = Arrangement.SpaceBetween
                            ) {
                                Text("${qty}x ${prod.name}", fontSize = 13.sp, color = Color(0xFFE2E8F0))
                                Text("$${String.format("%.2f", prod.price * qty)}", fontSize = 13.sp, color = Color.White, fontWeight = FontWeight.Medium)
                            }
                        }
                    }
                    HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp), color = Color(0xFF334155))
                }

                Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(bottom = 8.dp)) {
                    listOf("Card Tap", "Cash", "NFC Wallet").forEach { tender ->
                        FilterChip(
                            selected = paymentMethod == tender,
                            onClick = { paymentMethod = tender },
                            label = { Text(tender, fontSize = 12.sp) }
                        )
                    }
                }

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceBetween,
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Column {
                        Text("Tax (8%): $${String.format("%.2f", tax)}", fontSize = 11.sp, color = Color(0xFF94A3B8))
                        Text("Total: $${String.format("%.2f", grandTotal)}", fontSize = 20.sp, fontWeight = FontWeight.ExtraBold, color = Color(0xFF34D399))
                    }
                    Button(
                        enabled = cartEntries.isNotEmpty(),
                        onClick = {
                            cartEntries.forEach { (prod, qty) ->
                                val idx = catalog.indexOfFirst { it.id == prod.id }
                                if (idx != -1) {
                                    catalog[idx] = catalog[idx].copy(stock = (catalog[idx].stock - qty).coerceAtLeast(0))
                                }
                            }
                            completedOrders += 1
                            dailyRevenue += grandTotal
                            activeReceipt = ReceiptRecord(
                                orderNumber = 100 + completedOrders,
                                items = cartEntries,
                                subtotal = subtotal,
                                discount = discount,
                                tax = tax,
                                total = grandTotal,
                                paymentMethod = paymentMethod
                            )
                            cart.clear()
                        },
                        shape = RoundedCornerShape(12.dp)
                    ) {
                        Text("Charge $${String.format("%.2f", grandTotal)}", fontWeight = FontWeight.Bold)
                    }
                }
            }
        }
    }

    activeReceipt?.let { receipt ->
        AlertDialog(
            onDismissRequest = { activeReceipt = null },
            title = { Text("RECEIPT #${receipt.orderNumber}", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold) },
            text = {
                Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
                    Text("TENDER: ${receipt.paymentMethod}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
                    HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
                    receipt.items.forEach { (prod, qty) ->
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text("${qty}x ${prod.name}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
                            Text("$${String.format("%.2f", prod.price * qty)}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
                        }
                    }
                    HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
                    if (receipt.discount > 0) {
                        Text("VIP DISCOUNT: -$${String.format("%.2f", receipt.discount)}", fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color(0xFF34D399))
                    }
                    Text("TAX (8%):      $${String.format("%.2f", receipt.tax)}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
                    Text("TOTAL PAID:    $${String.format("%.2f", receipt.total)}", fontFamily = FontFamily.Monospace, fontWeight = FontWeight.ExtraBold, fontSize = 14.sp)
                }
            },
            confirmButton = {
                Button(onClick = { activeReceipt = null }) {
                    Text("Done / New Order")
                }
            }
        )
    }
}
Troubleshooting

How to Debug POS & Cart Glitches

Keep inventory state and currency math rock-solid with these three debugging patterns:

Stock Count Doesn't Redraw on Tile

When updating an item inside a mutableStateListOf, replace the instance with .copy() so Compose detects the change: catalog[idx] = catalog[idx].copy(stock = newStock)

Floating-Point Price Drift

Always format display totals to 2 decimal places or ask your AI for BigDecimal math: "Format all currency labels with String.format(Locale.US, '%.2f', amount) or BigDecimal RoundingMode.HALF_UP."

Tablet Split-Screen Layout

Make your register shine on 10-inch countertop tablets by prompting: "Use BoxWithConstraints so screens wider than 600.dp show a 2-pane split layout (Catalog Grid left, Ticket right)."

Extensions

The Vibe Prompt Vault

Copy-paste prompts to add cash change calculation, stock management, and ESC/POS thermal receipt reports.

๐Ÿ’ต Prompt: Add Quick Cash Tendered & Change Due Calculator
Prompt Template

"When the 'Cash' payment tender is selected in PosTerminalScreen, show quick-tap cash denomination chips (Exact, $10, $20, $50) plus a custom tendered input field. Automatically calculate and display 'Change Due' both in the checkout bar and on the printed receipt modal."

๐Ÿ“ฆ Prompt: Add Inventory Manager Modal to Restock & Create Items
Prompt Template

"Add a 'Manage Stock' button in the top bar that opens a ModalBottomSheet where the cashier can increment/decrement stock counts for existing PosProducts or add a brand-new product (Name, Category, Price, Initial Stock) to the catalog."

๐Ÿ–จ๏ธ Prompt: Add End-of-Shift Z-Report & Bluetooth ESC/POS Formatter
Prompt Template

"Store all completed ReceiptRecords in a state list and add a 'Shift Report' dialog that aggregates total gross sales, total discounts, tax collected, and revenue broken down by payment tender (Card Tap vs. Cash vs. NFC Wallet), formatted as a 32-column ESC/POS text block with a Copy/Share button."

We use cookies for ads and analytics.