How I Published Xpenly — My First App Store App

The full story of building and publishing Xpenly — a React Native expense tracker — on the App Store. Stack, architecture, recurring transactions, and the App Store submission process.

SP

Sandeep Prajapati

Full Stack Developer · Ambit Global

July 8, 20258 min read

iPhone showing a personal finance mobile app with charts and expense categories

Why I Built an Expense Tracker

I know. Every developer's first app is a to-do list or expense tracker.

But I had a specific problem: existing apps were either too simple (no insights) or too bloated (15 screens before you can add a transaction). I wanted something that opened fast, logged an expense in two taps, and showed me meaningful patterns.

That became Xpenly — now live on the App Store.


The Tech Stack

Framework:   Expo (managed workflow)
Language:    TypeScript
State:       Redux Toolkit + RTK Query
Charts:      Victory Native
Backend:     Supabase (PostgreSQL + Auth)
Storage:     AsyncStorage (local-first)
Build:       EAS Build + EAS Submit

Architecture: Local-First

The defining architectural decision was local-first. Transactions are stored in AsyncStorage immediately when added. Supabase sync happens in the background.

This means:

  • The app feels instant — no loading spinners to log a transaction
  • Works offline completely
  • Sync conflicts are resolved with last-write-wins (acceptable for personal finance)
// Add transaction: optimistic local write → background sync
async function addTransaction(tx) {
  // Step 1: Write to local store immediately
  dispatch(transactionsSlice.actions.addLocal(tx));

  // Step 2: Sync to Supabase in background (non-blocking)
  syncToSupabase(tx).catch((err) => {
    // Mark as pending sync — retry on next app open
    dispatch(transactionsSlice.actions.markPendingSync(tx.id));
  });
}

The Hardest Part: Recurring Transactions

Recurring transactions sound simple: "Every month on the 15th, add -$50 for gym membership."

Edge cases that almost broke me:

  • What if the user doesn't open the app for 3 months? Generate all 3 missed instances.
  • What if the recurrence day is the 31st and the month has 28 days? Clamp to last day of month.
  • What if the user edits the amount mid-series? Apply only to future instances.
function generateMissedOccurrences(recurring, lastGeneratedDate, today) {
  const occurrences = [];
  let current = nextOccurrence(lastGeneratedDate, recurring.frequency);

  while (current <= today) {
    occurrences.push({
      ...recurring,
      id: uuid(),
      date: clampToMonthEnd(current, recurring.dayOfMonth),
      isGenerated: true,
    });
    current = nextOccurrence(current, recurring.frequency);
  }

  return occurrences;
}

This runs on every app launch to catch up missed recurring transactions.


Publishing to the App Store: Step by Step

Step 1: Apple Developer Account $99/year. Create your App ID in App Store Connect. Enable the capabilities you need (if any).

Step 2: EAS Build

npm install -g eas-cli
eas login
eas build:configure
eas build --platform ios --profile production

EAS handles code signing, provisioning profiles, and certificates automatically. No Xcode required.

Step 3: Screenshots Apple requires screenshots in exact dimensions for each device class. I used a screenshot generator template (Sketch) to produce all required sizes from one design.

Step 4: Privacy Policy Required for any app. I hosted a simple HTML page on Vercel.

Step 5: EAS Submit

eas submit --platform ios --latest

Uploads the build directly to App Store Connect. Then in ASC: add screenshots, set age rating, set pricing (free), submit for review.

Review time: 24 hours for initial review, approved on the first try.


3 Things I Wish I'd Known

1. Screenshot dimensions are strict and unintuitive Apple's screenshot requirements are for specific device dimensions that don't match common phone sizes exactly. Use a tool, don't screenshot manually.

2. TestFlight beta is mandatory for finding real-world bugs I ran a 2-week TestFlight beta with 15 friends. Found 3 bugs I never would have caught in the simulator (keyboard overlapping input on iPhone SE, recurring transactions not generating on first launch after install, RTK Query cache not invalidating on Supabase sync error).

3. The privacy policy URL must work before you submit App Store review will reject your app if the privacy policy URL returns a 404. I learned this the hard way (not on Xpenly, but a friend's app).


What's Next for Xpenly

  • Budget goals with progress rings
  • Receipt photo scanning with GPT-4 Vision
  • iCloud sync as an alternative to Supabase

Download Xpenly on the App Store or see my full portfolio: buildbysandeep.dev

SP

Written by

Sandeep Prajapati

Full Stack Developer with 3+ years experience. Building enterprise AI systems, real-time platforms, and mobile apps. Currently at Ambit Global Solution.