kleamerkuri

kleamerkuri

Aug 13, 2026 · 20 min read

Lessons Learned From Shipping An App For The First Time

I said this post was coming back in the first VersoID post, when I mentioned a few App Store surprises and subscription plumbing horror stories I was saving for later, since unpacking them there would’ve buried the actual story.

This is that post, and there’s no code walkthrough here either. It’s the list of things that ate my time, broke my assumptions, and taught me something I wish I’d known before I started.

Hey! Android is live 🙌 Not through the Play Store (more on why in a bit), but through a direct APK download and an Uptodown listing under the VersoID name that’s currently pending review. You can find both on the download page with the Uptodown link updated once it’s ready.

Let’s get into it.

Building Free Trial Logic With Stripe and Apple Pay

I assumed a free trial was basically a toggle. Turn it on, give people 14 days, done, next problem.

Sometimes I’m terribly optimistic.

Of course, it’s not a toggle but a small system with its own rules, and the rules change depending on who’s handling the trial.

For example, an in-house trial (you build the logic yourself) behaves differently from a trial routed through a payment provider like Stripe or Apple Pay. To determine what type of trial to even use, you must consider:

  • Do you require payment info before the trial starts?
  • Does the subscription auto-activate the moment the trial ends, or does it require another action from the user?
  • What notifications fire, and when? Three days before the trial ends? The day of? What are the legal requirements around it?

Each of those questions isn’t a small toggle. It’s a fork in how the whole trial behaves, and picking wrong means rebuilding the flow instead of adjusting one setting.

Then there’s cancellation. A user needs to be able to back out mid-trial, and that action has to ripple through the same state the rest of the system depends on.

If they cancel on day 10 of a 14-day trial, does that state change instantly, or at the end of the window they were promised?

I picked instantly, mostly because it matched what I’d want as a user, but that meant the cancellation logic had to talk to the same status field everything else in the app reads from. And, in my usual fashion, I proceeded to make it even more complicated by allowing users to pause their trials.

The truth is that I went into this thinking it’d be a quick build, a nice-to-have premium feature that I see nearly everywhere, and it turned into its own logical architecture with state management across several variables I hadn’t planned for on day one.

Variables like trial status, payment method on file, days remaining, and whether the user had already been notified all had to stay in sync

But that’s not all.

Stopping Trial Abuse: What Happens if Someone Deletes Their Account and Signs Up Again?

What stops someone from burning through their free trial, deleting their account, and signing back up with a new email for another 14 days?

Nothing, unless you build something to stop it.

That’s a scenario you don’t think about until you’re specifically thinking through how someone might try to get around your own product, which is an uncomfortable but necessary exercise for a solo builder.

I had to think through what actually identifies a “user” for trial-eligibility purposes, and an email address alone isn’t enough, since that’s exactly what a new signup resets.

Device identifiers, payment method fingerprints where a provider makes that available, and a check against whether an account tied to related sign-in credentials has already used a trial all had to factor in.

Tip 🔥
If you’re building any kind of free trial, decide up front what “eligible for a trial” actually means at the account level, not just at the email level. I didn’t think about this until I was already deep into the general trial logic, and retrofitting an abuse check onto a flow that already assumed every new signup was trial-eligible was its own headache.

Related: Solo, No Team: This Is How VersoID Actually Got Built, where I first mentioned this was coming.

Supabase and the Cancellation Decision Tree for Pro Users

Canceling a subscription sounds like one action, but it’s actually a fork.

A user cancels, which downgrades them to the free tier. Fine.

But what happens to the Pro data they built up while they had access? Is it gone? Does it stay in Supabase, gated but intact, in case they come back?

I went with keeping it.

Pro data stays in Supabase and becomes inaccessible until the user upgrades again, and nothing gets deleted on downgrade.

That decision took real thought, because “just delete it” is simpler to build and much worse for the user who cancels by accident or comes back three weeks later. (Again, think of the different users and their behaviors, then count how many hairs you have left 😬)

Why Deciding on a Status Model Is Easier Than Building It

Now, the decision to retain and gate unsubscribed user data was the easy half. The hard half was the actual implementation.

Doing so meant a status definition that had to hold up under every case I could think of, plus a UI that responded correctly the moment that status changed.

I had to map out what “status” even meant for a given user at a given moment, then make sure every screen that checked that status agreed with every other screen.

Get that logic wrong in one place, and a user sees Pro features they shouldn’t, or loses access to something they’re still paying for!

Tip 🔥
Are you building tiered access solo? Then write the status definitions down before you touch the UI. I didn’t do this cleanly the first time, and re-threading status checks through screens I’d already built cost more time (and tokens) than planning it upfront would have.

Why Apple’s “Sign In With Apple” Requirement Forced a Rebuild

My original plan was Google sign-in only, one provider, one flow, and I figured that was the right call for a solo build. It creates fewer auth paths to test, fewer edge cases to defend against, fewer places for something to break.

Then I got to the part where I actually wanted to list on the App Store, and found out Apple requires apps that offer third-party sign-in to also offer Sign in with Apple.

It’s not a suggestion. It’s a listing requirement, and it applies whether or not Sign in with Apple fits the product you’re building.

So a decision I’d already made and built around had to be reopened. That meant a second auth provider wired into the same Supabase user model the Google flow already used.

It also meant testing both paths to make sure a user landed in the same place regardless of which one they picked.

I added Apple login not because it improved the product, but because the platform I wanted to be on required it.

That’s a pattern that shows up again and again in this post. Decisions I thought were mine to make turned out to be decisions the platform makes for you, and you only find out after you’ve built the other way.

The fix isn’t a tweak. It’s a redo, sitting on top of a system you already thought was finished.

Tip: This is where judicious and critical research followed by planning is vital at the very start. It was partly my fault for having gotten only surface level information. But, in my own defense, it’s what you’d expect for anyone dipping their toes in quick sand.

Apple Developer Program Fees vs. Google Play Listing Requirements

So, I went in with a plan to list on both the Apple App Store and the Google Play Store, without doing real research into what listing on either one actually requires 😬

To be perfectly honest, I thought the process of listing was far more streamlined than it is, and I found out the hard way, right at the point where I thought I was ready to ship.

The $99 Apple Fee, the $25 Google Fee, and the Legal Fine Print Nobody Mentions

Each platform has its own fee, and the two aren’t close. Apple charges $99 a year for a Developer Program membership while Google Play charges $25 once, with no renewal.

That’s before you get into commission rates on paid features.

Beyond the fee, each platform has its own:

  • testing steps for payment features
  • required legal language
  • constraints on price points

I glossed over all of it, and none of it showed up on my radar until I was staring at a listing form I couldn’t submit.

Note: For anyone who’s wondering, I did use AI (thanks Claude) for my initial planning phase and research. However, these are the very details that fall through the holes of trying to get a plan on a single conversation thread. You need to look at sources and, definitely, challenge AI research statements and assumptions.

Fixing that wasn’t cosmetic. It meant detecting which platform a user was on, then showing the right notices, buttons, pricing, and legal text for that platform specifically.

I understand now why some apps pick one platform and stay there.

Cross-platform code sounds like a gift until distribution reminds you the stores don’t share a rulebook, and that reminder stays quiet right up until you’re the one living it.

Google Play’s 12-Testers-for-14-Days Rule (and Why I Skipped It)

Imagine my surprise (and disbelief) when I see that personal developer accounts created after November 2023 have to run a closed test with at least 12 opted-in testers for 14 straight days before Google grants production access.

Testers have to actually open and use the app, not install it and forget about it.

This is what kept VersoID off Google Play at launch (even though I paid the $25).

Twelve real people, actively using an app for two weeks straight, isn’t something I can produce without hiring people to do it, and that wasn’t the plan for a solo side project.

So Android is live off-platform for now through a direct APK download plus an Uptodown listing, both linked in the intro.

Hey! The only reason I actually finished the Apple listing work at all is that I’d already paid the $99 fee. Sunk cost is a great motivator when the alternative is eating a fee for nothing 😅

Setting Up an Apple App Review Demo Account for Gated Features

Yet another thing I didn’t know going in was that Apple requires you to provide a working demo account, or a full-featured demo mode, if your app has account-based or gated features; otherwise, a reviewer can’t actually see what they’re evaluating.

That meant building a specific, secure way for reviewers to get into a Pro-tier account without going through the normal signup and payment flow.

I’m keeping the actual mechanism vague on purpose since it touches account security, but what I can say is that it involves a login triggered in a specific way, tied to credentials I control.

Seed data already sits in Supabase, so a reviewer sees a fully populated Pro experience the moment they’re in: personas, links, and analytics already filled out instead of an empty new-account state.

I hadn’t planned for this at all. It only surfaced once I read through what App Review actually checks, and by then the feature set was already built around the assumption that Pro access always ran through payment.

Nevertheless, it opened the floodgates since it turns out I actually needed a version of that for myself too, for a different reason.

Testing the Android payment and subscription flow through Stripe meant I needed a way to verify Pro access in production without actually paying for it every time I wanted to check something.

A reviewer needs to see the app as a paying user would. I needed to see it without becoming one, repeatedly, on my own dime. Go figure.

Tip 📍
If your app gates features behind payment, budget time for building two of these: one for reviewers, one for yourself. I didn’t plan for either until I hit the wall that made them necessary.

Designing a Liquid-Glass Dark Theme Without Breaking Accessibility

The visual direction I wanted was an ultra-premium, liquid-glass-inspired dark theme. I’m talking frosted panels, dark background, the whole aesthetic.

What I didn’t fully account for going in was how much of that decision comes back around to accessibility.

Low-contrast frosted panels on a dark background look great in a mockup, then fight you the moment real text needs to sit on top of them.

Every customization option tied to Pro also needed its own access logic, and that logic had to respond correctly the moment a user’s status changed.

Image cropping, image selection, background choices, opacity, color theme—none of it was one-size-fits-all either. Free users see a locked state on Pro-only options. Pro users see the full set.

Downgraded users go back to locked without losing what they’d already picked, which ties directly back to the retain-and-gate decision from the cancellation section 😵‍💫

These customizations also varied slightly by platform, since iOS and Android handle things like image picking and system-level color themes a little differently under the hood.

For instance, a cropping tool that feels native on iOS needed its own equivalent on Android, not a shared component pretending both platforms behave the same way.

None of this was purely a design problem. It was a design problem wrapped around the same status-and-gating logic from the cancellation section, applied to a different set of features. (That logic was on a constant loop.)

Explore: This Is A Super Easy Optimization Workflow For SEO & Accessibility

Flutter and Dart as a First Mobile App: The Simulator and Device-Size Reality

I build for the web, and I’m used to browser tools and extensions, a development loop that feels streamlined, at least compared to what came next. Reload the page, see the change. That’s most of the loop.

Mobile asked for more.

I needed simulators to get started, then a separate step later where I tested on an actual physical device, since a simulator alone isn’t enough to trust before shipping.

Some things, like push notification permissions or camera access, behave differently on real hardware than they do in a simulated environment. And since VersoID is cross-platform, that meant an Apple simulator for iOS-specific behavior and an Android simulator for Android-specific behavior.

Two separate environments, two separate sets of quirks, and two separate places for the same feature to behave slightly differently.

On top of that, there are device sizes to account for. Android alone spans a wide range of screen sizes, from small budget phones to large-format devices, and a layout that looks right on one can break on another.

Tablets need their own consideration on both operating systems too, since each OS treats tablet apps a little differently from phone apps. A screen built for a phone-sized viewport doesn’t scale up cleanly on its own.

None of this is unique to Flutter. It’s the tradeoff of mobile development generally, but it was new to me, and it added a testing dimension the web never asked me for.

Enabling iOS Capabilities in Xcode and the Apple Developer Portal

This one caught me off guard well into the process because of how it’s split across two different places.

For iOS, enabling certain capabilities like push notifications means going into Xcode, but that’s only half of it.

You also have to go into Apple’s developer portal to actually link those capabilities to the project and regenerate the provisioning profile that ties them together.

    Miss that second half and the capability looks enabled in your code but silently fails on a real device. It’s not a compile error; it’s a feature that doesn’t work, and nothing tells you why until you go looking.

    I hit this with push notifications where the code looked right, the Firebase setup looked right, and nothing arrived until I traced it back to a provisioning profile that hadn’t been regenerated after I’d toggled the capability in Xcode.

    That’s a real process, not a checkbox, and it chips away at the premise Flutter sells you on that you need one codebase to achieve cross-platform and, voilà, you’re done.

    Sure, the codebase part is true, but you still need OS-specific and platform-specific configuration underneath that shared code. (Even the darn splash screen needs separate handling!)

    Note 💬
    If you’re coming into mobile development without a background in it, that gap is where the “easy” part of cross-platform starts to erode. You find out about it one capability at a time, usually while wondering why a feature that compiled fine refuses to work. So, here I am to this day still trying to figure out how all non-coders are “vibe coding” their way across apps.

    Debugging Across Firebase, Stripe, Flutter, and Supabase Logs at Once

    Setting up Firebase for notifications and Stripe for payments wasn’t the surprising part because getting the accounts running is expected work for any app like this.

    What actually caught me off guard was what testing looked like once both were live and something went wrong.

    A single failed notification, for example, could trace back to Firebase not delivering it, Flutter not handling the payload correctly, Supabase not having the right user token on hand, or Stripe not firing the webhook that should’ve started the chain in the first place.

    I ended up debugging across four separate log streams at once:

    1. Stripe logs
    2. Firebase logs
    3. Flutter logs
    4. Supabase logs for auth and login

    Chasing one bug sometimes meant checking all four before I found where it actually lived.

    Mind you, none of these tools are hard to use on their own. The difficulty is that a bug rarely announces which one it belongs to, so every investigation starts by opening four tabs and starting to rule out things one at a time.

    A payment that doesn’t confirm could be Stripe not firing the webhook, or it could be Supabase failing to write the updated status after the webhook already fired correctly.

    Those look identical from the user’s side and completely different from the log side.

    Tip 🔥
    For anyone stacking multiple third-party services solo, expect your debugging surface to multiply with each one. A bug that looks like it’s in your code might be sitting one log stream away.

    The Double-Click Bug That Only a Real User Could Find

    When I shipped the first version of VersoID, I had a few friends use the app early so they could QA it, verify things worked, and flag anything worth improving.

    One of them told me they were getting a duplicate persona every time they created one. I went looking for the cause and couldn’t find it, since nothing in the logic should have produced a duplicate no matter how I traced it.

    I checked the creation flow line by line. I checked the Supabase writes. Then I checked whether it was a rendering issue where one persona displayed twice on screen.

    None of it matched what she was describing.

    I finally asked her to walk me through exactly what she did, step by step, instead of describing the result. Turns out it only happened if you clicked the create button twice in quick succession.

    That’s it. That’s the whole bug.

    I wouldn’t have thought to test for that on purpose, since my own test passes always click once, wait, confirm, move on. But a real person, moving through the app the way people actually do (a little impatient, maybe not even noticing they double-clicked), found it in minutes.

    It took a human being outside my own head to surface it 💁‍♀️

    The fix itself was small: disable the button on first click until the request resolves. What wasn’t small was what it revealed.

    I’d written tests for the logic of persona creation, but not for the mechanics of a real thumb on a real screen. Those are two different kinds of correctness, and I’d only been checking one of them.

    Why Solo QA Means Testing Like a Real User, Not Just Writing Tests

    Tests can only catch what you thought to write a test for. They check specific logical paths, but they don’t account for every scenario a real user will stumble into, and the double-click bug above is proof of that.

    So testing solo meant putting myself in the position of the person actually using the app. What would they click? What would they miss?

    What would they do out of order, on purpose or not?

    There’s a version of this that goes beyond QA, too. Day to day, in my actual engineering job, I work with a stack I know well.

    However, building VersoID meant working with Flutter and Dart, a stack I hadn’t touched before, leaning on AI to move through it faster than I could have alone.

    That’s not quite the “vibe code it” approach, since I still went in with a structured plan, but there’s a gap between building in something familiar and building in something you’re learning in real time.

    I felt that gap as hesitation more than once, asking myself: Am I actually following Flutter best practices here? What’s this doing to performance?

    I didn’t always have a confident answer, and that hesitation certainly didn’t go away once the app worked.

    Why? Because working code and correct code aren’t the same thing, and in a framework I didn’t have years of muscle memory in, I couldn’t always tell which one I was looking at.

    I leaned on research and AI-assisted review to close that gap where I could. But part of it was sitting with the discomfort of shipping something I couldn’t fully vouch for from experience alone.

    Related: Why AI-Generated Code Actually Fails And How To Fix It

    It’s a Wrap

    While the first VersoID post was about the idea and the shape of the build, this one’s about many things underneath it that never show up in a screenshot.

    None of these trials were the fun part.

    The fun part was building the persona stack, widget, and QR sharing.

    But the free trial logic, cancellation decision tree, store listings, reviewer accounts—that’s the work that actually decides whether an app makes it to real users or stalls out in a folder on your laptop, forever labeled as a “demo” or “prototype”.

    Android’s live now, off-platform for the moment, as an APK download and Uptodown link (pending review; will be updated once available). My Apple users can get it on the App Store anytime.

    And, all users, check out the features I’m talking about in the app while you read this! Talk about great visualization.

    For any solo builders (new or experienced), if you’ve hit one of these walls, I want to hear about it. What’s the trial you didn’t see coming?

    Thanks for reading and supporting.

    Cheers 🤙

    😏 Don’t miss these tips!

    We don’t spam! Read more in our privacy policy

    Related Posts

    Leave a Comment

    Your email address will not be published. Required fields are marked *