James Montemagno - James Montemagno https://staging.montemagno.com/author/james-montemagno/ https://staging.montemagno.com/content/images/2017/08/MonogramIcon_Orange_NoBuffer.png James Montemagno - James Montemagno https://staging.montemagno.com/author/james-montemagno/ Hugo -- gohugo.io Tue, 13 Oct 2026 20:48:55 GMT 60 Introducing PDF Diff: Compare PDFs Privately in Your Browser https://staging.montemagno.com/introducing-pdf-diff-compare-pdfs-privately-in-your-browser/ 69540d8693272a0001bc4c55 Wed, 31 Dec 2025 15:35:55 GMT

Hey friends! I'm super excited to share a new tool I've been working on that solves a problem I bet many of you have faced: how do you quickly compare two PDF documents to see what changed? Enter PDF Diff!

Landing page for PDF Diff allowing you to upload 2 PDFs to compare
Landing page for PDF Diff allowing you to upload 2 PDFs to compare

Why I Built This

Look, we've all been there. You get two versions of a contract, a proposal, or a license agreement, and you need to figure out what changed between them. Sure, you could upload them to some random website, but do you really want to send your potentially sensitive documents to a server somewhere? I know I don't.

So I built PDF Diff - a completely client-side, privacy-first PDF comparison tool that runs entirely in your browser. No servers. No uploads. No risk.

Privacy First, Always

This is the core principle that drove the entire development: your documents never leave your device. Period.

Everything happens locally in your browser using JavaScript. All the PDF parsing, text extraction, and comparison logic runs on your machine. Once you load the page, you can even disconnect from the internet and keep using it. That's the power of modern web technology, and it's beautiful.

What Can You Do With It?

I packed in a bunch of features to make comparing PDFs as easy and flexible as possible:

Multiple View Modes

Different people need to see changes in different ways, so I built five different view modes:

Side-by-Side View - This is the classic comparison view. Original on the left, modified on the right, with synchronized scrolling. Perfect for getting the full context of both documents.

Unified View - All changes shown inline with color coding. Green for additions, red for deletions. This is great when you want to see everything in one continuous flow.

Additions Only - Just show me what's new! This view filters out everything except the added content. Super useful when you're reviewing what someone added to a document.

Removals Only - The opposite - show only what was deleted. Great for catching what might have been taken out of a contract or agreement.

Changes Only - This one's clever - it shows additions and removals side-by-side, grouped by where they appear in the document. You get a focused view of just the changes without any of the unchanged content.

Real-Time Statistics

As soon as you upload your PDFs, you get instant statistics:

  • How many words were added
  • How many were removed
  • How many stayed the same
  • Overall change percentage

This gives you a quick sense of how significant the changes are before diving into the details.

Multi-Page Support

Documents aren't always single pages! PDF Diff handles multi-page documents with easy page navigation. You can step through page-by-page, or flip on "Show all pages" to see changes across the entire document at once.

Export to PDF

Found something important? Export your comparison as a formatted PDF report that you can save or share. The export includes your selected view mode, so you can generate exactly the report you need.

Dark Mode

Because of course it has dark mode. Your eyes will thank you. 🌙

Try the Demo

Not ready to upload your own PDFs yet? Hit the "Try Demo" button to see it in action with sample license agreement documents. It's the fastest way to understand how powerful this tool is.

The Technical Bits

For my fellow developers out there, here's what's under the hood:

Modern Stack

I built this with some of my favorite modern web technologies:

  • React 19 - The latest and greatest from the React team. The new concurrent rendering features make the UI incredibly smooth even when processing large documents.
  • TypeScript - Type safety is non-negotiable for me these days. It catches so many bugs before they make it to production.
  • Vite - Lightning-fast build tool that makes development a joy. Hot module replacement is so fast you barely notice it refreshing.
  • PDF.js - Mozilla's incredible PDF rendering engine. This is what powers the PDF parsing and text extraction, and it's rock solid.
  • jsPDF - For generating the export PDFs. Great library, easy to use.
  • diff - The text comparison algorithm at the heart of the whole thing. This library does the heavy lifting of computing what changed.

Architecture Decisions

The entire app is client-side only. No backend, no API, no database. Just static files served from a CDN. This architecture choice has some huge benefits:

  1. Privacy by design - Can't leak data to a server if there is no server
  2. Scales infinitely - Static files are cheap and can handle unlimited traffic
  3. Fast - No network round trips for processing
  4. Simple - No servers to maintain or databases to manage

The comparison logic is surprisingly straightforward - extract text from both PDFs, run a diff algorithm, and render the results. But getting the details right (handling multi-page docs, preserving formatting, making the UI responsive) is where the real work happens.

Built with Modern Tools

I have to give a huge shoutout to the tools that made building this such a smooth experience:

VS Code + GitHub Copilot

I've been using VS Code for years, and GitHub Copilot has become an indispensable part of my workflow. Writing the TypeScript interfaces, React components, and utility functions was so much faster with Copilot suggesting completions and entire functions.

The autocomplete for repetitive patterns (like all those view mode switches) saved me tons of time. And when I was implementing the different view modes, Copilot would often suggest exactly the logic I was thinking about. It's like pair programming with an AI that never gets tired.

GitHub Copilot Cloud Agent

Here's where things get really cool. I used the new GitHub Copilot Cloud agent to help with some of the more complex parts of the build. The agent can understand your entire codebase context and help you make architectural decisions, write tests, and even suggest optimizations.

For example, when I was working on the export functionality, I described what I wanted (a formatted PDF with the comparison results), and the agent helped me figure out the best way to structure the jsPDF calls and format the output. It understood the context of my existing code and suggested changes that fit perfectly with my patterns.

The Cloud agent also helped me optimize the PDF parsing logic for performance. When I noticed larger documents were taking a while to process, I described the problem, and it suggested some clever optimization strategies.

Open Source

The entire project is open source on GitHub. I'm a huge believer in open source, and I wanted anyone to be able to:

  • Verify that the privacy claims are true (inspect the code yourself!)
  • Contribute improvements or bug fixes
  • Learn from the code
  • Fork it for their own needs

Try It Out!

Ready to compare some PDFs? Head over to www.pdf-diff.com and give it a try. Start with the demo if you want to see it in action, or upload your own documents if you're ready to go.

I'd love to hear your feedback! Found a bug? Have a feature request? Want to contribute? Open an issue or submit a PR.

And if you find this useful, give it a ⭐️ on GitHub - it really helps others discover the tool.

What's Next?

I've got a few ideas for future improvements:

  • Support for comparing more than two documents
  • Highlighting specific types of changes (numbers, dates, etc.)
  • Better handling of PDFs with images and complex layouts
  • Browser extensions for quick access

But I want to hear from you - what would make PDF Diff more useful for your workflow?

Wrapping Up

Building PDF Diff has been a fun journey, and I'm proud of how it turned out. It solves a real problem I had, and I hope it helps you too. The combination of modern web technologies, privacy-first design, and powerful developer tools made it possible to build something genuinely useful in a relatively short time.

The web platform has come so far. The fact that we can parse PDFs, compute diffs, and render beautiful UIs entirely in the browser - without any server-side code - is pretty amazing. And tools like GitHub Copilot and the Cloud agent make the development experience better than ever.

Give it a try and let me know what you think!

Cheers, James

]]>
Introducing Oh My Posh Visual Configurator: Finally, a Drag-and-Drop Terminal Theme Builder! ✨ https://staging.montemagno.com/introducing-oh-my-posh-visual-configurator-finally-a-drag-and-drop-terminal-theme-builder/ 6952bf0593272a0001bc4431 Mon, 29 Dec 2025 19:43:35 GMT

If you've ever spent way too much time digging through JSON files trying to get your terminal prompt just right, this post is for you. I'm incredibly excited to share something I've been working on: the Oh My Posh Visual Configurator—a web-based drag-and-drop builder that makes creating beautiful terminal prompts actually fun (yes, really!).

The main interface showing the segment picker, configuration canvas, and live preview
The main interface showing the segment picker, configuration canvas, and live preview

The Problem We All Know Too Well

Let me be honest with you—I love Oh My Posh. It's hands down one of the best tools for customizing your terminal prompt across PowerShell, Bash, Zsh, Fish, and basically any shell you can think of. But here's the thing: configuring it has always meant diving into JSON, YAML, or TOML files and doing a lot of trial-and-error to get segments positioned correctly.

You know the drill:

  1. Edit the config file
  2. Save it
  3. Open a new terminal to see if it worked
  4. Realize the colors clash
  5. Go back to step 1
  6. Repeat for the next hour

There had to be a better way.

Enter the Visual Configurator

The Oh My Posh Visual Configurator solves this problem with a modern, intuitive web interface. No installation required—just open your browser and start building!

👉 Launch the Configurator

What Can You Do With It?

🎨 Browse 103+ Segments {#%F0%9F%8E%A8-browse-103-segments}

The left sidebar gives you access to every segment Oh My Posh supports, organized into logical categories:

  • System: Path, OS, Shell, Session, Battery, Time, Execution Time
  • Version Control: Git, Mercurial, SVN, Fossil, and more
  • Languages: Node.js, Python, Go, Rust, Java, .NET, PHP, Ruby... basically everything
  • Cloud & Infrastructure: AWS, Azure, GCP, Kubernetes, Docker, Terraform
  • CLI Tools: NPM, Yarn, Angular, React, Flutter
  • And more: Music players, health trackers, weather widgets
The Languages category expanded showing 26+ programming language segments
The Languages category expanded showing 26+ programming language segments

🖱️ Drag-and-Drop Interface {#%F0%9F%96%B1%EF%B8%8F-drag-and-drop-interface}

Just click a segment to add it to your prompt, or drag it exactly where you want it. Reordering is as simple as dragging segments around. Want to move that Git status before your path? Just drag it there!

Click any segment to customize its colors, style, and template
Click any segment to customize its colors, style, and template

⚡ Live Preview {#%E2%9A%A1-live-preview}

This is where it gets really cool. Every change you make shows up instantly in the preview panel at the bottom. Toggle between dark and light terminal backgrounds to make sure your theme looks great everywhere. The preview even renders powerline arrows and diamond shapes accurately!

🎛️ Full Customization {#%F0%9F%8E%9B%EF%B8%8F-full-customization}

Click any segment and the Properties Panel opens up with everything you need:

  • Style Selection: Powerline, Plain, Diamond, or Accordion
  • Color Pickers: Full hex color support for foreground and background
  • Template Editor: Customize exactly what each segment displays using Go templates
  • Block Settings: Configure prompt alignment, add new blocks, or create right-aligned prompts

📦 Import & Export {#%F0%9F%93%A6-import-export}

Already have an Oh My Posh config? Import it! The configurator supports JSON, YAML, and TOML formats. When you're done, export in whichever format you prefer and drop it right into your shell configuration.

Community Sharing 🤝 {#community-sharing-%F0%9F%A4%9D}

One of my favorite features is community sharing. Found the perfect configuration? Share it with everyone!

Browse official samples and community-contributed themes
Browse official samples and community-contributed themes
  1. Click the Share button in the header
  2. Fill in your theme details—name, description, tags
  3. Submit your configuration
  4. Your theme appears in the Community collection for others to use
Easy-to-use sharing dialog with step-by-step instructions
Easy-to-use sharing dialog with step-by-step instructions

This means you can browse themes created by other developers, try them instantly in the configurator, and tweak them to match your style. The community grows together!

Under the Hood: Some Fun Architecture Decisions 🏗️ {#under-the-hood-some-fun-architecture-decisions-%F0%9F%8F%97%EF%B8%8F}

For my fellow developers who enjoy the technical details, here's what's powering this thing:

React 19 + TypeScript + Vite

I went with the latest React 19 for this project, paired with TypeScript for type safety (trust me, when you're dealing with complex configuration objects, types are your friend). Vite provides blazing-fast builds and hot module replacement that makes development a joy.

@dnd-kit for Drag-and-Drop

After evaluating several drag-and-drop libraries, @dnd-kit won out. It's modern, accessible, performant, and plays nicely with React 18+. The sortable preset made implementing segment reordering straightforward, and it handles edge cases like keyboard navigation out of the box.

Zustand for State Management

I'm a big fan of Zustand for state management. It's lightweight, has zero boilerplate, and the persistence middleware lets me save your configuration to localStorage automatically. Your work is never lost!

// Simplified example of our store structure
const useConfigStore = create(
  persist(
    (set) => ({
      config: defaultConfig,
      addSegment: (blockId, segment) => set((state) => /* ... */),
      updateSegment: (blockId, segmentId, updates) => set((state) => /* ... */),
      // ...
    }),
    { name: 'ohmyposh-config' }
  )
)

Dynamic Segment Loading

With 103+ segments, loading everything upfront would be wasteful. Instead, segment metadata lives in separate JSON files by category (public/segments/*.json), loaded on demand when you expand a category. This keeps the initial bundle small and the UI snappy.

Tailwind CSS 4.1

Styling is handled by Tailwind CSS 4.1, which gives us:

  • Consistent design tokens
  • Dark theme by default (it's a terminal tool after all!)
  • Rapid iteration on UI polish
  • Small production bundles through automatic purging

100% Client-Side

Your configurations never leave your browser. Everything runs locally—no backend servers, no data collection, no privacy concerns. Export your config and it's yours forever.

Built with GitHub Copilot & VS Code

Here is what blows my mind about this app is that I built it in under an hour. I started by jotting down a product requirements doc in the GitHub mobile app using Copilot, based on a simple idea. Then I created an issue, assigned it to Copilot, and went to bed. By the next morning, I had a working prototype! I spent about 50 minutes in VS Code adding features like community feeds and cleaning up the code for future updates. Copilot even set up all the GitHub Actions workflows and deployed everything to GitHub Pages in seconds. Turning an idea into a live app in just hours—something that used to take me weeks—is absolutely incredible.

Getting Started in 30 Seconds

  1. Open the app: https://jamesmontemagno.github.io/ohmyposh-configurator/
  2. Add segments: Click segments from the left sidebar or drag them to the canvas
  3. Customize: Click any segment to adjust colors, templates, and styles
  4. Preview: Watch your prompt update in real-time at the bottom
  5. Export: Choose JSON, YAML, or TOML and download your configuration
  6. Apply it:

# PowerShell oh-my-posh init pwsh --config ~/your-theme.json | Invoke-Expression

# Bash eval "$(oh-my-posh init bash --config ~/your-theme.json)"

# Zsh eval "$(oh-my-posh init zsh --config ~/your-theme.json)"

That's it! You've got a beautiful, custom terminal prompt.

Sample Configurations to Get You Started

Not sure where to begin? We've included 6 professional sample configurations:

  • Minimal: Clean and simple
  • Developer: Language-aware with Git integration
  • DevOps: Cloud and Kubernetes focused
  • Full Featured: All the bells and whistles
  • Powerline Classic: Traditional powerline style
  • Diamond Style: Rounded segment separators
Community-contributed themes available in the Theme Library
Community-contributed themes available in the Theme Library

Load any of these from the Sample Picker and customize from there!

What's Next?

I'm really excited about where this project can go. Some ideas on the roadmap:

  • More starter templates based on community feedback
  • Theme galleries curated by the community
  • Advanced template editor with autocomplete
  • PWA support for offline use
  • Keyboard shortcuts for power users

Try It Out and Let Me Know!

I'd love to hear what you think. Give it a spin at https://jamesmontemagno.github.io/ohmyposh-configurator/ and let me know how it goes!

Found a bug? Have a feature request? Open an issue on GitHub.

Created an awesome theme? Share it with the community using the Share button!

Happy coding, and may your terminal always look beautiful! 🚀


This blog was drafted with GitHub Copilot and Claude Sonnet 4.5 based off previous blogs. Screenshots were taken with the Playwright MCP server using the cloud agent completely autonomously. I reviewed this blog post and tweaked it as necessary.

]]>
Building a Complete FIRE Calculator App with GitHub Copilot in One Chat Session https://staging.montemagno.com/building-a-complete-fire-calculator-app-with-github-copilot-in-one-chat-session/ 694b62804480ae000170634b Wed, 24 Dec 2025 16:13:01 GMT

I've been diving deep into financial independence planning lately, and as a developer, I naturally wanted a tool that does exactly what I need—without the clutter, ads, or privacy concerns of existing calculators out there. So I decided to build one from scratch using GitHub Copilot's agent mode in VS Code. What started as a simple "let's see what happens" experiment turned into a fully-featured, privacy-first PWA with 9 different calculators, all built in a single conversational chat session in about 30 minutes.

The Starting Point

I had a clear vision: a FIRE (Financial Independence, Retire Early) calculator that respects user privacy. No cookies, no analytics, no data leaving the browser. Just pure client-side calculations with the ability to share scenarios via URL parameters.

My initial prompt was straightforward leveraging the plan agent in VS Code:

"I want to build a financial independence calculator app. It should be a React + TypeScript PWA with Tailwind CSS, dark mode support, and work completely offline. Privacy is key—no data storage, no analytics."

The plan agent got to work asking me questions along the way until it had the context it needed and I answered enough questions that I felt good about it getting to work. From there, Copilot scaffolded the entire project structure: Vite config, TypeScript setup, Tailwind with a custom "fire" orange color palette, and even service worker configuration for offline support. Within minutes I had a running dev server.

Iterating Through Features

What I love about working with Copilot in agent mode is the conversational flow. I didn't have to context-switch between planning and implementing. Each feature request built naturally on the previous one.

The Core Calculators

I started with the Standard FIRE calculator—the classic "25x your annual expenses" rule. Copilot generated not just the calculation logic but also:

  • Responsive input components with tooltips explaining each field
  • A beautiful projection chart using Recharts
  • Real-time URL parameter syncing so users can bookmark or share their scenarios
  • Result cards with the key metrics highlighted

Then came the variations:

"Now add a Coast FIRE calculator. Same structure but with the specific Coast FIRE math—how much do you need saved NOW so compound growth does the rest."

"Let's add Lean FIRE (≤$40k expenses) and Fat FIRE ($100k+ expenses) with appropriate warnings and lifestyle tips."

"Add a Barista FIRE calculator that factors in part-time income."

Each calculator maintained consistent styling while having its own personality—different color schemes, contextual explanations, and tailored result displays.

The Fun Additions

Halfway through, I asked for three more calculators that I hadn't seen done well elsewhere:

"Add these: 1) Savings Rate calculator that shows how your savings rate impacts years to FIRE, 2) Reverse FIRE that works backwards from a target retirement age, 3) Healthcare Gap calculator for estimating costs between early retirement and Medicare."

Copilot handled the math, the UI, and even added helpful reference tables and scenario comparisons. The Healthcare Gap calculator includes ACA subsidy considerations and strategies like health sharing ministries and Barista FIRE for benefits.

Real-Time Bug Fixes and Refinements

This is where the conversation got interesting. I noticed the Coast FIRE progress card was showing "2160%" instead of "21.5%". A quick:

"The Coast FIRE current progress card is showing an extremely high number—double check those calculations."

Copilot traced through the code and found the issue: the percentage was being multiplied by 100 twice—once in the calculation and again in the ResultCard's percent formatter. Fixed in seconds.

Another issue: the age input would snap to 18 whenever you tried to clear it to type a new value. Annoying UX.

"Entering the retirement age is weird because it sets it to 18 when I try to clear it out. Just don't calculate if invalid but allow entry."

The fix added local state management so users can freely type without the input fighting back, with validation only on blur.

Privacy and Polish

I asked for disclaimers on every page—this isn't financial advice, results are hypothetical, consult a professional. Copilot created a reusable Disclaimer component and added it to all 10 pages in one pass.

The app includes:

  • No cookies or localStorage - Your financial data never leaves your browser
  • No analytics - Zero tracking scripts, no Google Analytics
  • URL-based sharing - Save calculations in the URL, bookmark or share as you choose
  • Offline-first PWA - Works without internet after first load
  • Open source - All code available on GitHub for transparency

Quality of Life Features

Throughout the session, I kept adding small enhancements:

"Add a monthly/annual toggle to the contribution and expenses inputs."

"Put a progress bar showing how far along you are toward your FIRE number."

"Add milestone lines at 25%, 50%, 75% on the projection charts."

"Quick preset buttons for common scenarios—conservative, moderate, aggressive."

Each request was implemented cleanly, maintaining the existing code style and patterns.

The Tech Stack

For those interested in the technical details:

  • Vite + React 18 with TypeScript
  • Tailwind CSS with custom fire-orange color palette
  • Recharts for interactive projection graphs
  • React Router for navigation with URL parameter persistence
  • Vite PWA Plugin for service worker and offline support
  • GitHub Pages for hosting

The entire codebase is well-organized:

src/
  components/
    charts/      # Recharts wrappers
    inputs/      # CurrencyInput, PercentageInput, AgeInput
    ui/          # Cards, Buttons, ProgressBar, Disclaimer
  pages/         # Each calculator as a page component
  hooks/         # useCalculatorParams for URL sync
  utils/         # All the FIRE math calculations

Key Takeaways

Conversational development works. Instead of writing detailed specs upfront, I could explore ideas naturally. "What if we also showed..." or "Actually, can you change that to..." kept the momentum going.

Copilot handles the boring stuff. Adding disclaimers to 10 pages? Tedious. Watching Copilot do it correctly in one multi-file edit? Satisfying.

Bugs get fixed in context. When something didn't work right, I could describe the symptom in plain English and Copilot would trace through the logic to find the root cause.

The result is production-ready. This isn't a prototype or demo—it's a real app I'm actually using to plan my own financial future.

Try It Yourself

The app is live at https://myfirenumber.com and the source is on GitHub.

All 9 calculators are there:

Calculator What it does
🎯 Standard FIRE Classic 25x expenses rule
⛵ Coast FIRE How much you need now so growth does the rest
🌿 Lean FIRE FI with minimalist lifestyle (≤$40k/year)
💎 Fat FIRE FI without compromising ($100k+/year)
☕ Barista FIRE Part-time work + portfolio income
📊 Withdrawal Rate Test portfolio longevity at different rates
🧮 Savings Rate How savings rate impacts time to FIRE
🔄 Reverse FIRE Work backwards from target retirement age
🏥 Healthcare Gap Costs between early retirement and Medicare

Play with the parameters, bookmark your scenarios, and let me know what you think. And if you're curious about building your own apps with Copilot agent mode, just start a conversation—you might be surprised how far it takes you.


This blog was written with VS Code and Claude Sonnet 4.5 based on a conversational chat session. I also reviewed and slightly modified the content.

]]>
Building Better Apps with GitHub Copilot Custom Agents https://staging.montemagno.com/building-better-apps-with-github-copilot-custom-agents/ 69078e67ebf2b1000161ff74 Sun, 02 Nov 2025 17:36:04 GMT

When building Hotkey Typer, a Windows Forms app that types predefined snippets via hotkey, I discovered the power of GitHub Copilot's new custom agents feature. What started as a simple refactoring request turned into an eye-opening demonstration of how specialized AI agents can dramatically improve code quality and implementation speed.

What Are Custom Agents?

Custom agents are specialized versions of GitHub Copilot coding agent that you can tailor to your unique workflows, coding conventions, and use cases. Instead of repeatedly providing the same instructions and context, custom agents allow you to define specialized agents that act like tailored teammates—following your standards, using the right tools, and implementing team-specific practices.

Think of custom agents as expert consultants embedded directly in your development workflow. They're defined using simple Markdown files (called agent profiles) that specify:

  • Name and description: What the agent specializes in
  • Custom prompts: Detailed instructions that define the agent's behavior and expertise
  • Tools: Specific tools the agent can access (optional)
  • MCP servers: Model Context Protocol server configurations for enhancing agents

Where You Can Use Custom Agents

Once created, custom agents are available wherever you use Copilot coding agent:

  • GitHub.com: In the agents tab, panels, issue assignments, and pull requests
  • Visual Studio Code: As chat modes in the editor
  • GitHub Copilot CLI: Command-line interactions

Each interaction maintains consistency based on the agent's defined profile, ensuring targeted support tailored to your specific needs.

Screenshot 2025-11-02 at 9.32.11 AM.png

Where to Configure Custom Agents

You can define agent profiles at different levels:

  • Repository level: Place .md files in .github/agents/ for project-specific agents
  • Organization level: For broader team availability
  • Enterprise level: For company-wide standardization

The agent profile format is straightforward:

---
name: readme-creator
description: Agent specializing in creating and improving README files
---

You are a documentation specialist focused on README files. Your scope is limited to
README files or other related documentation files only - do not modify or analyze code files.

Focus on the following instructions:
- Create and update README.md files with clear project descriptions
- Structure README sections logically: overview, installation, usage, contributing
- Write scannable content with proper headings and formatting
- Add appropriate badges, links, and navigation elements

Discovering the WinForms Expert Agent

The real power of custom agents became apparent when I discovered the awesome-copilot repository, a curated collection of custom agents maintained by GitHub. This repository contains specialized agents for various frameworks and technologies, including C#, WinForms, arm Migration, Dynatrace, LaunchDarkly, Neon, PagerDuty, and more!

Among these gems, I found the WinForms Expert agent—a sophisticated agent specifically designed for .NET Windows Forms development. This agent encodes decades of WinForms best practices into a single, reusable profile.

What Makes the WinForms Expert Special?

The WinForms Expert agent is a comprehensive guide that understands the nuanced challenges of WinForms development. It addresses several critical areas:

Designer Code vs. Regular Code: One of WinForms' unique challenges is managing two distinct code contexts—designer-generated serialization code and modern business logic. The agent knows exactly which C# features are allowed in each context.

Modern .NET Patterns: The agent is fully updated for .NET 8-10, including:

  • MVVM data binding patterns with the Community Toolkit
  • Async/await patterns with proper InvokeAsync overload selection
  • DarkMode support and high-DPI awareness
  • Nullable reference types (NRTs) in the right places

Layout Best Practices: Comprehensive guidance on using TableLayoutPanel and FlowLayoutPanel for responsive, DPI-aware layouts that work across different screen sizes and scaling factors.

CodeDOM Serialization Management: Critical rules for property serialization in the WinForms designer, preventing common pitfalls with [DefaultValue] attributes and ShouldSerialize*() methods.

Exception Handling: Proper patterns for async event handlers and application-level exception handling to prevent process crashes.

The agent's instructions are remarkably detailed, covering everything from control naming conventions to accessibility requirements. It's like having a senior WinForms architect reviewing every line of code.

Adding the WinForms Expert to My Repository

Adding a custom agent from the awesome-copilot repository is remarkably simple:

  1. Create the .github/agents/ directory in your repository
  2. Copy the agent profile (a .md file) into this directory
  3. Commit and push—that's it!

For Hotkey Typer, I copied the WinFormsExpert.agent.md file into .github/agents/, making it immediately available to Copilot throughout my repository.

The Real-World Test: Multiple Snippets Feature

I had opened Issue #3 requesting a new feature: support for managing multiple reusable text snippets instead of just a single predefined block. Users needed to switch between different prompts, code templates, and demo scripts quickly without manual settings editing.

The feature requirements included:

  • Maintain a list of snippets with names and content
  • Select active snippet from a dropdown in the main UI
  • New/Copy/Rename/Delete operations with validation
  • Backward-compatible migration from the old single-text format
  • Persist everything in settings.json

First Attempt: Without the Custom Agent (PR #7)

I first assigned the standard Copilot coding agent to Issue #3, resulting in Pull Request #7. The implementation was functional:

  • ✅ Data model created with Snippet class
  • ✅ CRUD operations implemented
  • ✅ UI with ComboBox and management buttons
  • ✅ Unit tests added
  • ✅ Settings serialization worked

However, there were notable issues:

  • Layout wasn't optimal for WinForms best practices
  • Controls weren't properly anchored/docked for responsive resizing
  • Missing some WinForms-specific patterns for dialog handling
  • Code style didn't fully align with established WinForms conventions
  • No consideration for DPI scaling or accessibility

While functional, it felt like code written by someone proficient in general C# but not deeply experienced with WinForms specifics.

Second Attempt: With the WinForms Expert Agent (PR #8)

Then I closed PR #7 and reassigned Issue #3 to Copilot, but this time explicitly mentioning the WinForms Expert agent. The results in Pull Request #8 were dramatically better:

Improved UI Organization:

Snippet: [────────Dropdown (300px)────────▼]
         [New] [Copy] [Rename] [Delete]

[Text Editor (460x135)]

[Speed slider, checkboxes, file options]

[Save]
[Minimize] [Stop]        Status: Active

The agent reorganized the layout following WinForms best practices:

  • Logical grouping of snippet management operations
  • Proper spacing and margins (3px minimum)
  • Controls properly anchored for responsive behavior
  • Consideration for DPI scaling

Enhanced User Experience:

  • Popup dialogs using InputDialog for New/Copy/Rename operations
  • Proper validation with meaningful error messages
  • Case-insensitive duplicate detection
  • Protection against deleting the last snippet
  • Better visual feedback for operations

Code Quality Improvements:

  • Modern C# 11-14 features in business logic
  • Proper separation between designer code and regular code
  • File-scoped namespaces
  • Target-typed new expressions
  • Nullable reference types in appropriate contexts
  • Exception handling with ArgumentNullException.ThrowIfNull

Even Better Documentation: The agent included detailed diagrams of the UI layout directly in the PR description, making the implementation immediately understandable.

Key Code Differences: Before and After

Let me show you some concrete examples of the improvements the WinForms Expert agent made:

1. InputDialog Helper Class

PR #7 (Without WinForms Expert): Used an inline dialog creation pattern with repetitive code in each button handler.

PR #8 (With WinForms Expert): Created a dedicated, reusable InputDialog class following WinForms best practices:

internal class InputDialog : Form
{
    private readonly TextBox txtInput;
    private readonly Button btnOK;
    private readonly Button btnCancel;

    public string InputText => txtInput.Text;

    public InputDialog(string prompt, string title, string defaultValue = "")
    {
        Text = title;
        FormBorderStyle = FormBorderStyle.FixedDialog;
        StartPosition = FormStartPosition.CenterParent;
        MaximizeBox = false;
        MinimizeBox = false;
        ClientSize = new Size(350, 120);

        // Control setup...

        // Select all text when dialog opens
        Load += (s, e) => txtInput.SelectAll();
    }

    public static string? Show(string prompt, string title, string defaultValue = "")
    {
        using var dialog = new InputDialog(prompt, title, defaultValue);
        return dialog.ShowDialog() == DialogResult.OK ? dialog.InputText : null;
    }
}

This is a textbook WinForms pattern: proper disposal with using, nullable return type, static factory method, and proper dialog configuration.

2. Settings Migration Pattern

PR #7 (Without WinForms Expert): Used a "fresh start" approach that ignored existing settings if loading failed.

PR #8 (With WinForms Expert): Implemented proper backward-compatible migration:

// Migration: Convert old single PredefinedText to snippet if no snippets exist
if (settings.Snippets == null || settings.Snippets.Count == 0)
{
    string contentToMigrate = !string.IsNullOrEmpty(settings.PredefinedText)
        ? settings.PredefinedText
        : DefaultSnippetContent;
    snippets = new List<TextSnippet>
    {
        new TextSnippet
        {
            Id = DefaultSnippetId,
            Name = DefaultSnippetName,
            Content = contentToMigrate,
            LastUsed = DateTime.Now
        }
    };
    activeSnippetId = DefaultSnippetId;
}
else
{
    snippets = settings.Snippets;
    activeSnippetId = settings.ActiveSnippetId;

    // Validate active snippet exists
    if (string.IsNullOrEmpty(activeSnippetId) ||
        !snippets.Any(s => s.Id == activeSnippetId))
    {
        activeSnippetId = snippets.FirstOrDefault()?.Id;
    }
}

This handles the upgrade path gracefully, preserving user data instead of discarding it.

3. UI Layout in Designer Code

PR #7 (Without WinForms Expert): Controls placed with absolute positioning and cramped spacing:

var btnNewSnippet = new Button
{
    Text = "New",
    Location = new Point(335, 82),
    Size = new Size(35, 25),  // Cramped button
    Font = new Font("Segoe UI", 8F)
};

PR #8 (With WinForms Expert): Better organized with logical grouping and proper sizing:

// Snippet selector ComboBox - wider, more prominent
var cmbSnippets = new ComboBox
{
    Name = "cmbSnippets",
    Location = new Point(85, 82),
    Size = new Size(300, 23),  // 300px width for readability
    DropDownStyle = ComboBoxStyle.DropDownList,
    Font = new Font("Segoe UI", 9F)
};

// Snippet management buttons - moved under dropdown for logical grouping
var btnNewSnippet = new Button
{
    Name = "btnNewSnippet",
    Text = "New",
    Location = new Point(85, 110),
    Size = new Size(70, 25),  // Proper button size
    Font = new Font("Segoe UI", 9F)
};

The WinForms Expert agent understood that:

  • ComboBoxes should be wide enough to display meaningful names
  • Related buttons should be grouped together visually
  • Font size 8F is too small for modern displays
  • Controls need descriptive names for maintainability

4. Validation Logic with User Feedback

PR #7: Basic validation without proper user feedback:

if (string.IsNullOrWhiteSpace(name)) return;
if (snippets.Any(s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
    MessageBox.Show("A snippet with this name already exists.",
        "Duplicate Name", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    return;
}

PR #8: Enhanced validation with trim and separate empty-string check:

// Validate: trim and check uniqueness (case-insensitive)
name = name.Trim();
if (string.IsNullOrEmpty(name))
{
    MessageBox.Show("Snippet name cannot be empty.",
        "Invalid Name", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    return;
}

if (snippets.Any(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)))
{
    MessageBox.Show("A snippet with this name already exists.",
        "Duplicate Name", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    return;
}

The WinForms Expert understood the importance of:

  • Trimming user input (common WinForms pattern)
  • Providing specific error messages for different failure modes
  • Using string.Equals for clarity over extension method

5. Modern C# Features in Business Logic

PR #8 consistently used modern C# patterns throughout:

// File-scoped namespace
namespace HotkeyTyper;

// Target-typed new
private List<TextSnippet> snippets = new();

// Expression-bodied member
public string InputText => txtInput.Text;

// Nullable reference types
public static string? Show(string prompt, string title, string defaultValue = "")

// Using declaration (proper disposal)
using var dialog = new InputDialog(prompt, title, defaultValue);

While PR #7 had some modern features, PR #8 applied them consistently and appropriately.

6. Comprehensive Unit Tests

PR #8 included thorough migration tests that PR #7 lacked:

[Fact]
public void OldSettings_MigratesToSnippets()
{
    // Arrange - Old settings format with single PredefinedText
    string oldSettingsJson = """
    {
        "PredefinedText": "Hello, World!",
        "TypingSpeed": 7,
        "HasCode": true
    }
    """;

    var oldSettings = JsonSerializer.Deserialize<AppSettings>(oldSettingsJson);

    // Act & Assert - Settings should be migrated
    Assert.NotNull(oldSettings);
    Assert.Null(oldSettings.Snippets);
    Assert.Equal("Hello, World!", oldSettings.PredefinedText);
}

These tests ensure the backward compatibility works correctly—a crucial detail for production applications.

The Visible Difference

The before-and-after images tell the story:

Without WinForms Expert:

  • Basic layout, functional but not polished
  • Controls not optimally positioned
  • Missing WinForms-specific refinements

With WinForms Expert:

  • Clean, logical organization
  • Professional spacing and alignment
  • Proper control grouping
  • Responsive layout that scales correctly

The agent even provided visual diagrams showing the layout structure, demonstrating deep understanding of WinForms design patterns.

The Impact: Quality and Velocity

The difference between PR #7 and PR #8 wasn't just cosmetic. The WinForms Expert agent delivered:

Better Architecture:

  • Proper use of BindingSource patterns for data binding
  • Correct designer code serialization patterns
  • Appropriate use of modern C# features in the right contexts
  • Following established WinForms conventions consistently

Fewer Iterations:

  • PR #7 required multiple rounds of feedback and corrections
  • PR #8 was nearly perfect on the first attempt
  • Code reviews focused on business logic, not framework usage

Knowledge Transfer:

  • Seeing the agent's implementation taught me WinForms patterns I hadn't fully mastered
  • Comments in the code explained why certain approaches were used
  • The detailed PR descriptions served as documentation

Time Savings:

  • What would have taken days of iteration took hours
  • Less time debugging designer code issues
  • More time focusing on features, not framework quirks

Getting Started with Custom Agents

Ready to try custom agents in your projects? Here's how:

1. Explore the Awesome-Copilot Repository

Visit github.com/github/awesome-copilot and browse the available agents.

2. Add an Agent to Your Repository

  1. Create .github/agents/ in your repository
  2. Copy the agent .md file from awesome-copilot
  3. Customize if needed (optional)
  4. Commit and push

3. Use the Agent

In GitHub issues, pull requests, or VS Code:

  • Mention the agent by name in your request
  • For issues: Assign Copilot and reference the agent
  • For VS Code: Use <agent-name> in chat from agent drop down

Screenshot 2025-11-02 at 9.33.00 AM.png

4. Create Your Own Agents

Have specific team conventions? Create custom agents that encode:

  • Your coding standards
  • Framework-specific patterns
  • Architecture preferences
  • Testing strategies
  • Documentation requirements

Full documentation: GitHub Docs - Creating Custom Agents

Key Takeaways

Working with the WinForms Expert agent taught me several valuable lessons:

Specialization Matters: Generic AI coding assistance is good, but specialized agents that understand framework-specific patterns are transformative. The depth of knowledge in the WinForms Expert agent far exceeded what I could convey in ad-hoc prompts.

Consistency Across the Team: Custom agents ensure everyone on your team follows the same patterns and best practices, even for developers less familiar with specific frameworks.

Living Documentation: Agent profiles serve as executable documentation of your team's coding standards—they don't just describe patterns, they enforce them.

Knowledge Amplification: Using specialized agents is a learning experience. Seeing high-quality, pattern-consistent code generated repeatedly helps developers internalize best practices.

Velocity Without Sacrificing Quality: The right custom agent doesn't just generate code faster—it generates better code faster, reducing technical debt and future maintenance burden.

Wrapping Up

The difference between PR #7 (without custom agent) and PR #8 (with WinForms Expert) perfectly illustrates why custom agents represent a significant leap forward in AI-assisted development. It's not about replacing developer expertise—it's about amplifying it with domain-specific knowledge that would otherwise require years of experience to accumulate.

For Hotkey Typer, the WinForms Expert agent helped deliver a feature that not only works but follows industry best practices, is maintainable, accessible, and properly architected for future enhancements. The implementation handles edge cases, uses appropriate design patterns, and would pass a senior code review with minimal comments.

If you're working with WinForms—or any specialized framework—I highly recommend exploring custom agents. The awesome-copilot repository is an excellent starting point, and creating your own team-specific agents is easier than you might think.

Learn More

Custom agents aren't just the future of AI-assisted development—they're available today, and they're already making a significant difference in how we build software. Give them a try, and see the difference specialized expertise can make in your projects!


Have you tried custom agents in your projects? I'd love to hear about your experiences. Share your thoughts and questions on X!

This blog was written with VS Code and Claude Sonnet 4.5 based on a very lovely prompt with images, MCP server access, and more written by ME! I also reviewed and slightly modified the blog.

]]>
5 Ways I Use Background Coding Agents Every Day https://staging.montemagno.com/5-ways-i-use-background-coding-agents-every-day/ 68d814b64825cf00016b2e62 Wed, 01 Oct 2025 17:46:09 GMT

Coding agents are different from in-editor chat and agent modes for AI assisted programming. They run asynchronously—often in the cloud—tackling tasks while you focus locally. I rely heavily on the GitHub Copilot coding agent and its VS Code integration to spin up multiple parallel workflows. Below are five everyday patterns I use to offload work, speed up development, and keep my repos in top shape.


1. Assign Issues Directly to GitHub Copilot

When a new issue or idea lands, I immediately hand it off to Copilot so it can start coding while I tackle higher-level planning or work on any tasks. I have worked on huge issues, small bugs, and complete re-designs with this flow. What is great is that you can give it a lot of context in the issue to help guide it along. You can see everything directly in VS Code, stop it, ask it for changes, and even get a diff view without leaving the code editor.

By assigning an issue to the coding agent:

  • Checks out the base branch (usually main) and creates a feature branch named after your issue.
  • It installs dependencies, runs any setup workflows, and begins implementing the requested feature or fix.
  • A pull request pops up automatically, complete with commit history, a description of what was done, and live session logs you can follow.
  • I can review diffs right in VS Code’s Agent sessions view or directly on GitHub before merging.

Key benefits:

  • Parallelize work: start multiple issues at once without context-switching overhead.
  • Instant feedback: see what Copilot built, ask follow-up questions in the same session.
  • Consistency: every branch follows your CI/CD and code-style rules from day one.

Easy setup:

Learn more: • GitHub Copilot Coding Assistant overview: https://docs.github.com/en/copilot/concepts/agents/coding-agent • VS Code integration with GitHub Pull Requests & Issues: https://code.visualstudio.com/docs/copilot/copilot-coding-agent


2. Delegate Ad-Hoc Tasks from VS Code Chats

Spotted an improvement in your code or UI? Instead of stopping to implement it, or open an issues, you can simply delegate from your chat panel or from GitHub.com and get back to flow. This is maybe one of my favorite ways to use coding agents as I can even spin up a

How it works:

  • In VS Code, open Chat Sessions (enable via Settings > Chat: Agent Sessions View Location and set to view).
  • Under
  • Type a request like “When user clicks Hacker News, auto-load articles in content-feed.” or whatever else you want it to do. You can even reference files and documents.
  • Click Delegate to coding agent send button —Copilot spins up a background session, generates code, and opens a pull request.
  • Monitor progress in the Chat Sessions view or jump into the PR on GitHub.

Why I love it:

  • Lightweight delegation and works of a specific branch that you choose.
  • Rich context: chat history and your codebase give Copilot full visibility.
  • Notification-driven: get pinged when the PR is ready, so you only interrupt flow once.

Tip: You can also trigger these tasks from the mobile apps, GitHub.com on your repo, or the Copilot dashboard at https://github.com/copilot/agents without hopping into each repo.


3. Continue Work Across Branches and Chat Sessions

Sometimes I am working on a bug or new features and I have it half-implemented locally. Or sometimes I ideate in Ask mode and don't really want to open up an issue or work on it right now. Well, you can simply push your local changes and chat context into a new agent session so Copilot can finish while you move on.

Step by step:

  1. On your feature branch (or main), start a Copilot chat to outline next steps (e.g., add docs, refactor helpers).
  2. Make local edits but don’t commit everything.
  3. Click Delegate to coding agent, choose Include changes, and let Copilot fork a sibling branch with your pending code.
  4. Copilot completes the work—documentation, tests, cleanup—and issues a pull request ready for review.

Advantages:

  • Seamless handoff: your uncommitted work becomes the starting point for the agent.
  • Branch-of-branch workflow: keep your main branches tidy while Copilot wraps up.
  • No more “oh right, I forgot to finish that” moments—wake up to a completed PR.

4. Iterate on Pull Requests with Interactive Comments

When I work with the coding agents it isn't always going to get it right the first time. Sometimes it is the coding agent, sometimes it is because I didn't give it enough context, and sometimes I just ended up wanting it a bit different . I like to treat Copilot like a teammate and file a review comment and then watch it push fixes in real time.

Workflow:

  • Open a PR that Copilot has been working on in GitHub or in the chat sessions in VS Code and comment @copilot with error messages or changes you want.
  • Copilot launches a new agent session, applies fixes or enhancements, and updates the same branch.
  • You can reply to the agent’s comments, request tweaks, or approve and merge when satisfied.

Perks:

  • Instant turn-around on feedback.
  • Team-like collaboration: follow the same comment-reply flow you’d use with a colleague (or yourself in my case often).
  • Full history: every change is tracked in the PR conversation and commit log.

Docs to explore: • Using Copilot in pull requests: https://docs.github.com/en/copilot/extend-your-editor-with-copilot-chat#use-copilot-in-pull-requests


5. Keep Dependencies Secure and Up-to-Date with Dependabot

Dependabot isn’t AI-driven, but it embodies the same “background agent” principle. It quietly opens PRs to bump libraries and fix vulnerabilities.

Setup highlights:

  • In your GitHub repos Settings > Security > Advanced Secuirty, enable Dependency graph, Dependabot alerts, and Dependabot security updates.
  • Create a .github/dependabot.yml to specify package ecosystems (npm, NuGet, etc.), directory paths, and update schedules.
  • Dependabot will open pull requests whenever a new version is available or a security issue is discovered.
  • Review, merge, or auto-merge based on your repo’s policies.

Why it matters:

  • You don’t have to remember every library you depend on—they get updated for you.
  • Security vulnerabilities get flagged and patched without manual intervention.
  • Your CI/CD stays green with minimal overhead.

Get started: https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically


Setting Up Your Environment for Background Coding Agents

Before you reap these benefits, configure both GitHub and your local IDE:

  1. Enable the Copilot Coding Agent
  2. Install VS Code Extensions
  3. Activate Chat & Agent Sessions
    • In VS Code Settings, search “Agent Sessions View Location” and switch from Disabled to View.
    • This adds the Chat Sessions icon for monitoring all local and active background tasks.
  4. Add Copilot Setup Workflows (optional but recommended)
  5. Configure Dependabot
    • Add a .github/dependabot.yml specifying ecosystems, directories, and schedules.
    • Enable security updates and auto-merge rules under Settings > Advanced Security.

With those pieces in place, you’ll have a fleet of coding agents— GitHub Copilot and Dependabot—working around the clock to keep your code moving forward.


How are you leveraging background agents in your workflow? Hit me on X or leave a comment on YouTube and let me know.

]]>
Announcing Hotkey Typer — type long prompts with a hotkey https://staging.montemagno.com/announcing-hotkey-typer/ 68d715b04825cf00016b2592 Fri, 26 Sep 2025 22:57:22 GMT When I'm recording demos or making videos I often need to reproduce long prompts or blocks of text on-screen. Manually retyping them every time is slow and error-prone, and pasting from the clipboard can feel jerky and reveal private content. I wanted a tiny, reliable app that would let me press a global hotkey and have the application type a predefined, human-like block of text into whatever app currently has focus — perfect for demos, tutorials, and repeatable workflows.

Enter Hotkey Typer: a small Windows Forms utility that types back long prompts using a single global hotkey.

What Hotkey Typer does

  • Register a global hotkey (defaults to CTRL+SHIFT+1) that works across applications
  • Type a predefined text snippet into the currently focused window with human-like timing
  • Save your snippet and typing speed to a settings.json file for persistence
  • Minimize to the system tray so it runs unobtrusively in the background

This is intentionally minimal — it solves one problem very well: quickly and reliably retyping long prompts during recordings.

Quick feature tour

  • Global hotkey: Press CTRL+SHIFT+1 from any running application to trigger typing.
  • Human-like typing: Characters are sent one-by-one with randomized delays so the result looks natural on screen.
  • Typing speed slider: Choose from Very Slow through Very Fast (1–10) to simulate different typing paces.
  • Persistent settings: Your text and speed are saved to settings.json in the app directory.
  • System tray: Minimize the UI and keep the app running in the tray with a context menu to show or exit.

How I built it (and how GitHub Copilot + VS Code helped)

This little app was built as a Windows Forms app targeting .NET 10. I used Visual Studio Code as my editor and relied on GitHub Copilot to accelerate routine code — generating method bodies, serialization snippets, and the interop signatures for the Windows API calls.

Why Copilot was helpful here:

  • Saved time writing boilerplate: form initialization, settings load/save, and JSON serialization were scaffolded quickly.
  • Suggested idiomatic C# snippets: small things like the randomized delay logic and the speed-to-label mapping were easier to iterate on.
  • Encouraged small, testable functions: I split concerns into LoadSettings/SaveSettings, InitializeSystemTray, TypePredefinedText, and so on.

I used VS Code for quick editing and testing, and dotnet CLI for building and running the app.

How to build and run

Clone the repo and run it with the .NET SDK installed (Windows 10+):

git clone https://github.com/jamesmontemagno/app-hotkeytyper.git
cd app-hotkeytyper/HotkeyTyper
dotnet build
dotnet run

The app will start and register the default hotkey. Enter your desired text, click Update Text, then switch to any application and press CTRL+SHIFT+1 to have the app type your snippet.

If you want the app to always run in the background, minimize it to the tray.

Implementation notes — how some features work {#implementation-notes-%E2%80%94-how-some-features-work}

Here are a few implementation details for anyone curious or wanting to extend the app.

  • Global hotkey registration

    The app uses the Windows API functions RegisterHotKey and UnregisterHotKey via P/Invoke. The main form registers a hotkey during construction and handles the hotkey event in WndProc by checking for WM_HOTKEY and the registered id.

    Example P/Invoke signatures used in Form1.cs:

    [DllImport("user32.dll")]
    private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);
    
    [DllImport("user32.dll")]
    private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
    
    private const int WM_HOTKEY = 0x0312;
    private const int HOTKEY_ID = 1;
    
  • Typing the text

    When the hotkey fires the app calls an async TypePredefinedText() method. It uses SendKeys.SendWait() to send characters one-by-one to the foreground window and Task.Delay() with a randomized interval to simulate human typing. A short initial delay ensures the hotkey release doesn't interfere with the first character.

    A simplified version of TypePredefinedText() looks like:

    private async void TypePredefinedText()
    {
        SendKeys.Flush();
        await Task.Delay(250); // ensure hotkey is released
    
        int baseDelay = 310 - (typingSpeed * 30);
        int variation = Math.Max(10, baseDelay / 3);
        Random random = new Random();
    
        foreach (char c in predefinedText)
        {
            SendKeys.SendWait(c.ToString());
            SendKeys.Flush();
            await Task.Delay(Math.Max(20, random.Next(Math.Max(10, baseDelay - variation), baseDelay + variation)));
        }
    }
    
  • Timing and realism

    Typing speed is an integer from 1 (slowest) to 10 (fastest). The app converts that value into a base delay and adds a bit of random variation per character. This creates a natural rhythm instead of a machine-gun paste.

  • Settings persistence

    User configuration (the predefined text and the typing speed) are serialized to settings.json in the application directory using System.Text.Json. The app loads settings on startup and writes them whenever the user updates the snippet or changes speed.

    The simple settings DTO used for JSON (and its defaults):

    public class AppSettings
    {
        public string PredefinedText { get; set; } = string.Empty;
        public int TypingSpeed { get; set; } = 5;
    }
    
  • System tray

    A NotifyIcon with a ContextMenuStrip lets the app hide in the tray and provide quick actions (Show / Exit). The form's Dispose method unregisters the hotkey and disposes of the tray icon to keep the system state clean.

Extending Hotkey Typer — ideas {#extending-hotkey-typer-%E2%80%94-ideas}

  • Support multiple named snippets and a configurable hotkey per snippet
  • Add a CLI mode for scripting or remote triggering
  • Use the Windows Input Injection APIs for applications that block SendKeys
  • Add an option to send the entire string as a clipboard paste (with clipboard restoration) for faster input when acceptable

Notes and security

Because Hotkey Typer sends keystrokes to other applications, some security software may flag it as automation. This is expected for tools that simulate input — distribute and run it only in environments you control.

Also, typing into elevated apps may require Hotkey Typer to run with the same elevation level.

Isn't this in ZoomIt?

Why yes it is! I couldn't remember if this was added into some tool that I use, so I vibed coded it in 10 minutes :)

Fun fact, it is in SysInternals/ZoomIt which is also available in PowerToys and works pretty similar.


Thanks for reading — hope Hotkey Typer makes your demos smoother!

]]>
Introducing Bingo-o-matic: Your Ultimate Bingo Management App https://staging.montemagno.com/introducing-bingo-o-matic-your-ultimate-bingo-management-app/ 6834c49620e2c90001f0aa79 Mon, 26 May 2025 19:58:22 GMT

Are you tired of managing bingo games with paper cards and manual tracking? Look no further! I'm thrilled to announce the release of Bingo-o-matic, a comprehensive bingo management application designed to make hosting and playing bingo games easier and more enjoyable than ever before.

What is Bingo-o-matic?

Bingo-o-matic is a modern, user-friendly application that digitizes the entire bingo experience. Whether you're hosting a family game night, running a community fundraiser, or organizing a classroom activity, Bingo-o-matic provides all the tools you need to create, manage, and play bingo games with minimal effort.

Play traditional number-based bingo or unleash your creativity with fully customizable bingo sets! From holiday themes to educational concepts, company events to baby showers - the possibilities are endless. Create your own unique bingo experience tailored to any occasion or audience.

Key Features

  • Standard and Custom Bingo: Play classic numbered bingo or create themed sets with your own content
  • Generate Random Bingo Cards: Instantly create unique bingo cards for all your players
  • Print Custom Cards: Easily print physical cards for your custom bingo sets
  • Real-time Calling Tool: Call out bingo items with an intuitive interface
  • History Tracking: Keep track of called items to verify winning cards
  • Multiple Theme Options: Choose from light and dark themes for comfortable viewing
  • Offline Support: Play anytime, anywhere, even without an internet connection
  • Mobile-Friendly Design: Enjoy a seamless experience on any device

Screenshots

Standard Bingo mode with traditional numbered board Classic B-I-N-G-O with standard numbered calling interface

Custom Bingo interface with themed content Play with customized content perfect for themed events and educational settings

Custom bingo set creation interface Easy-to-use editor for creating and saving your own custom bingo sets

Getting Started

Getting started with Bingo-o-matic is incredibly simple:

  1. Visit https://www.bingo-o-matic.com
  2. Create a new bingo set from scratch or browse through our collection of pre-made templates to save time
  3. Generate as many unique cards as you need for your players - each card is randomized for fair play
  4. Use the intuitive calling interface to manage your game and track progress in real-time

Install as a Progressive Web App

One of the most exciting features of Bingo-o-matic is its Progressive Web App (PWA) capabilities. This modern web technology allows the application to function like a native mobile or desktop app while still being accessible through any web browser.

What does this mean for you?

Installing Bingo-o-matic as a PWA gives you several advantages:

  • Faster loading times - The app caches resources locally for instant access
  • Offline functionality - Continue playing even when your internet connection is spotty
  • App-like experience - Opens in its own window without browser navigation bars
  • Easy access - Pin it to your taskbar, dock, or home screen for quick launching

How to install:

  1. Navigate to https://www.bingo-o-matic.com using any modern web browser
  2. On desktop computers: Look for the install icon (usually a download or plus symbol) in your browser's address bar, or check the browser menu for "Install Bingo-o-matic"
  3. On mobile devices: Tap the share button and select "Add to Home Screen" from the menu options
  4. Once installed, Bingo-o-matic will appear in your applications and can be launched just like any other app

The PWA functionality ensures that Bingo-o-matic works reliably in any environment - from remote locations with poor connectivity to busy venues with crowded networks. Your bingo games can continue uninterrupted! Learn more about Progressive Web Apps on web.dev.


Behind the Scenes: The Technical Side of Bingo-o-matic

Technology Stack and Architecture

Bingo-o-matic leverages cutting-edge web technologies to deliver a seamless, high-performance experience. At its core, the application is built using Blazor WebAssembly, Microsoft's web UI framework that allows developers to build interactive web applications using C# instead of JavaScript.

Why Blazor WebAssembly?

  • Performance: Runs at near-native speed in the browser through WebAssembly
  • Type Safety: C#'s strong typing system reduces runtime errors
  • Ecosystem: Access to the entire .NET ecosystem and NuGet packages
  • Developer Experience: Rich tooling and debugging capabilities

Technical Components:

Development Workflow and Tools

The entire development process was revolutionized by leveraging AI-powered development tools:

Visual Studio Code served as the primary development environment, providing excellent Blazor debugging capabilities and integrated terminal access for .NET CLI commands.

GitHub Copilot Agent Mode wrote pretty much all of the code by providing intelligent code completions and suggestions that understood the context of Blazor components, helping accelerate development while maintaining code quality standards. It created the base app features, added new features, implemented advanced logic and styles, and so much more... including this blog post!

GitHub Copilot Coding Agent assisted with architectural decisions, best practice implementations, and complex problem-solving scenarios specific to Progressive Web App development all directly from GitHub.com.

This AI-assisted development approach reduced development time significantly while ensuring adherence to modern web development patterns and Blazor-specific best practices.

Cloud Infrastructure and Deployment

Azure Static Web Apps provides the hosting infrastructure with several key benefits:

  • Automated CI/CD Pipeline: GitHub Actions automatically build and deploy the application whenever code is pushed to the main branch
  • Global Content Delivery: The app is distributed across Microsoft's global CDN network, ensuring fast load times worldwide
  • Automatic HTTPS: Built-in SSL certificates provide secure connections without additional configuration
  • Custom Domain Support: Easy integration with custom domains and DNS management

The deployment process is fully automated - from code commit to live production deployment typically takes less than 3 minutes.

Progressive Web App Implementation

The PWA features are implemented through:

  • Service Worker: Handles caching strategies and offline functionality
  • Web App Manifest: Defines how the app appears when installed on devices
  • Cache Strategies: Intelligent caching of static assets and API responses
  • Background Sync: Ensures data consistency when connectivity is restored

I welcome contributions and feedback from the community to make Bingo-o-matic even better!

]]>
Developing & Working with .NET Containers on Windows ARM Devices https://staging.montemagno.com/developing-working-with-net-containers-on-windows-arm-machines/ 65a9f8471270dd000126e7cb Fri, 19 Jan 2024 18:46:22 GMT

I am by no means a cloud-native developer, I am a client developer. I have spent some time though to get a general understanding about microservices, images, containers, docker, and all the buzz words that surround the topic. I recently got really into DevContainers and using them on GitHub Codespaces and Visual Studio Code to create an environment to run apps in. From there, I went down a rabbit hole of educating myself more by taking Microsoft Learn training courses, watching the new containers for beginner series, and actually trying to build something myself.

However, this is where I always seem to get into trouble as my main work machine every day where I am doing training is a Windows ARM device. I have been using the Project Volterra device for a long time and I simply love it. The ecosystem of what I can develop and my day to day is ALMOST identical to my normal x86 machine. One area as of time of writing this that always gets me is Docker and the lack of support Docker Desktop on Windows ARM (although it is coming in the future). DevContainers... needs Docker.... container development...needs Docker.... and there we have the issue.... or do we...

Enter WSL

Yes, the Window Subsystem for Linux to the rescue! We are creating Linux containers anyways so might as well run them in WSL! Just like the Windows Subsystem for Android enables me to develop Android apps easily, WSL does the same for container and dev containers. DevContainers was a simple solution, simply install Docker inside of Ubuntu on WSL and BOOM you are good to go. So elegant, so simple.

However, creating a container image with the dotnet publish -t:PublishContainer command is not so easy. In an ideal world I would just run this on my Windows machine for my .NET app, it would create the image, and then I could view it in Docker Desktop. Well.... no docker desktop. So, I could use WSL to run the command INSIDE of Ubuntu and then use Docker inside of WSL to run it! WHOA! That would work... if there was a distribution of Ubuntu out of the box that supported installing .NET 8 SDK for Ubuntu ARM easily. You technically need Ubuntu 23.10 to do this, and easy install of Ubuntu is only at 22.04. Now YES you can totally install any distro of Linux into WSL... but I like the "easy" button. Additionally, since I still don't have any Docker Desktop in this scenario, I would still have to use the Docker command line for management.

Enter Podman & Podman Desktop

But what if I told you don't need to have Docker to run images and containers. They are standard and other things can run them do such as Podman which is available on Windows, works by using WSL under the hood, and works on ARM devices!!!Oh, it also is super easy to install with just a normal installer!

After you have it installed, all you need to do is run podman machine init and start it up with podman machine start.

Now, since I love GUIs, it is time to install Podman Desktop that will easily help you boot podman, view images, create and run containers, and pretty much anything else you would want to do. It will also walk you through the complete set-up and installation process as well if you haven't done that yet or need to make changes in the future.

Podman Desktop home page

Let's create a .NET container!

Now, the fun part here is that I can use all my normal Windows tools including the .NET CLI, VS Code, or Visual Studio to create my images with the .NET SDK and run them in podman. Podman just sits in the background and handles the logistics. I don't have to worry about signing into WSL or anything.

Now, we can create a new .NET web app with dotnet new web -o HelloPodman.

Then it is time to publish it with dotnet publish -t:PublishContainer.

This should give you some output like this:

HelloPodman -> C:\Users\jamont\Desktop\HelloPodman\bin\Release\net8.0\HelloPodman.dll
  HelloPodman -> C:\Users\jamont\Desktop\HelloPodman\bin\Release\net8.0\publish\
  Building image 'hellopodman' with tags 'latest' on top of base image 'mcr.microsoft.com/dotnet/runtime:8.0'.
  Pushed image 'hellopodman:latest' to local registry via 'podman'.

Now, let's run it! I could run it manually with podman commands in my normal Windows Terminal with any shell including normal cmd. Or I could open up Podman Desktop and create a new container from the image.

When you hit the play icon it will take you to the configuration screen where it will be pre-configured with the dotnet entry point and 8080 for port:

Create the container and then you can launch it!

There you have it! Your Windows ARM machine is configured and ready to roll to develop containers and microservices with .NET!

]]>
Using Java SDK 11 on Xamarin Android Builds on Azure DevOps & GitHub Actions https://staging.montemagno.com/java-sdk-11-azure-devops-github-actions/ 6578dab01270dd000126e748 Tue, 12 Dec 2023 22:45:42 GMT Everything is building great in your CI/CD pipeline and then BOOM your build fails for no reason. You didn't change anything in your code!?!?! That was me this morning as I was updating my InAppBilling Plugin for .NET. I got hit with this beauty:

error XA0031: Java SDK 11.0 or above is required when using $(TargetFrameworkVersion) v13.0

What is going on?!?!? Starting with Visual Studio 2022 17.8, which was just released, the Xamarin.Andriod tooling requires Java SDK 11.... but the default images on Azure DevOps still have the default set to an earlier version. No worries! as you can manually set this with the Java tool installer task as the first step in the process.

Here is the YAML:

steps:
- task: JavaToolInstaller@0
  displayName: 'Use Java 11'
  inputs:
    versionSpec: 11
    jdkArchitectureOption: x64
    jdkSourceOption: PreInstalled

If you are using GitHub Actions, you can use the actions/setup-java action first: https://github.com/actions/setup-java

After that you are good to go!

]]>
Developing Android Apps on Windows Arm Devices https://staging.montemagno.com/developing-android-apps-on-windows-arm/ 644961ecfaa091003df357e4 Thu, 27 Apr 2023 23:00:11 GMT

I have gone all in on Arm devices! My main MAC machine is a Mac Book Air M1 and now my main driver at work is a Windows Dev Kit 2023 (aka Project Volterra). One tricky thing for developers is getting all the software we use to work properly on Arm. Things have come a long way since the original release of Arm devices for Mac and Windows. I have a full setup guide for my M1 for Xamarin and now .NET MAUI development, but what about Windows?

Visual Studio 2022 recently released a full native Arm version and they have been adding more workloads for developers to use. .NET MAUI was recently introduced, which got me excited but I right away ran into an issue, which was that there are no compatible Android emulators for Windows Arm and apparently no plans from Google :(. So, what are our options? Two come to mind.

Deploy to a device

Well, it may be obvious, but you can just plug in a device and use something like Vysor to mirror your device. I am always a fan of deploying and testing on devices as you have full access to everything including Google Play services. As a bonus you don't drag down any resources on your main machine. If you don't have a device, you are still in luck!

Windows Subsystem for Android

That's right! I have blogged and talked about it a bunch, but the Windows Subsystem for Android(WSA) is a great way to deploy and debug applications on any Windows 11 machine. It has expanded into more regions since its first release, which is awesome. If you haven't heard about WSA, it is a mode that you can enable that allows you to run Android applications directly on your Windows machine. There are limitations of course, the largest being no access to Google Play services, but for a lot of developers this will get you off the ground running. Checkout my full video:

If you are using Visual Studio 2022 there is a great extension that will help you get up and running and automatically connect.

Once you have it all set up, it is ready and available!

So, there you go! Two ways to get deploying Android apps if you are developing on a Windows Arm device.

]]>
Automate Posting to Mastodon via Web Requests https://staging.montemagno.com/automate-posting-to-mastodon-via-web-requests/ 638ad2a6e86a03003db428f1 Sat, 03 Dec 2022 04:51:13 GMT One of my favorite applications that I use for automation is If This Then That aka IFTTT. I have been using it forever and it allows you to automate a plethora of things when something happens. For example, maybe you want ot create a spreadsheet of every favorited tweet you have, or maybe save them to a list somewhere, or maybe automatically turn on your lights at sunset. All of this is possible with IFTTT and is crazy easy to use with connectors. I often have automation setup on RSS feeds or my YouTube feed when I post something that it automatically Tweets it out and posts it to a Discord channel:

There are all sorts of built in connectors such as Twitter, which makes it really easy to setup automation. However, services such as Discord and Mastodon don't have these integrations.... they do however have an API that you can call through the web request feature of IFTTT. So, let's get started!

Get your API key

Head over to your Mastodon server, go to settings, and then Development. For example, this is what the URL looks like on mastodon.social: https://mastodon.social/settings/applications

Create a New application and set the following:

  • Application name: IFTTT
  • Application website: https://www.ifttt.com
  • Redirect URI: leave alone
  • Scopes: uncheck everything except - write:statuses
  • Save changes

Once create you will get 3 different keys. The one that you want is Your access token. Copy this as you will need this soon.

Create your webhook in IFTTT

When you setup your IFTTT, you will want to have it trigger by something, in my instance I posted on my rss. Then choose a "that" which is "Webhooks".

Enter the following:

Make sure to update mastodon.social with your server name and also put your API key in there ;)

Here is what my Body looks like for my blog

status=Just posted a new blog: {{EntryTitle}} - {{EntryUrl}}

Test it out!

You did it!!!! Mission successful!!!

]]>
Enabling Users to Manage Subscriptions on iOS/Android https://staging.montemagno.com/enabling-users-to-manage-subscriptions-on-ios-android/ 632f2485ae7288003d7930aa Sat, 24 Sep 2022 16:01:17 GMT

You know I love talking about in app purchases and subscriptions. Due to my InAppBilling library I get all the "fun" updates when Apple or Google force developers to make changes to stay compliant. Upcoming is a nice large change from Google to push Android developers to use Billing v4 (which my library does!) and another is that we must now give users a way to easily manage and cancel their subscriptions. You may have recently seen this message from google:

We’re updating our Subscriptions policy to state that users must be able to easily cancel their subscriptions from within the app. {#we%E2%80%99re-updating-our-subscriptions-policy-to-state-that-users-must-be-able-to-easily-cancel-their-subscriptions-from-within-the-app}

What does this mean? What do I need to do Google?!?!! Well if you dig through the documentation you will see that you have to add a button to your purchase screen and also create a deep link to launch the store management screen. Let's see what this looks like in code for Xamarin and .NET MAUI apps:

public async Task ManageSubscription(string sku)
{
    var url = string.Empty;
    if (DeviceInfo.Platform == DevicePlatform.iOS)
    {
        url = "https://support.apple.com/HT202039";
    }
    else if (DeviceInfo.Platform == DevicePlatform.Android)
    {
        if (string.IsNullOrWhiteSpace(sku))
        {
            url = "https://play.google.com/store/account/subscriptions";
        }
        else
        {
            url = $"https://play.google.com/store/account/subscriptions?sku={sku}&package={AppInfo.PackageName}";
        }
    }

    if (string.IsNullOrWhiteSpace(url))
        return;

    await Browser.OpenAsync(url);
}

For iOS there isn't really a requirement or even an API to give users an easy way to manage subscriptions, but I figure why not.

For Android, there are two cases here:

  1. They don't have any non-expired subscriptions and you just launch the generic page.
  2. Your user has a non-expired subscriptions for your app, where expiryTime is in the future or autoRenewing is set to true. In this case you would want to open the subscription page to the very specific subscription for them to manage.

And, that is about it... Well actually you could simplify this entire process by creating a Manage App Subscriptions page on your website and linking to that. Then your code becomes:

await Browser.OpenAsync("https://refractored.com/manage-app-subscriptions");

This should also adhere to the app store guidelines and also you can add additional information. Cheers!

Checkout my videos on YouTube

]]>
App Configuration Settings in .NET MAUI (appsettings.json) https://staging.montemagno.com/dotnet-maui-appsettings-json-configuration/ 62195c853cfb01003d2c9dbd Fri, 25 Feb 2022 23:11:36 GMT

.NET MAUI is right around the corner and Preview 13 is out right now! There are so many great features packed into .NET MAUI and my favorite has to be all of the great Dependency Injection and configuration that goes into the MauiProgram startup. If you want to know more checkout my most recent YouTube video breaking down Preview 13 and all of the configuration that is built in including Essentials, Blazor, and more.

But, what if you want to do other things that aren't included out of the box... such as reading app configuration through a appsettings.json file?!?!? Well you can, and it is super easy! Thanks to the MauiAppBuilder we can use the ConfigurationManager that is built in to configure settings in our .NET MAUI app. First things first, let's add the appsettings.json file as an EmbeddedResource:

{
  "Settings": {
    "KeyOne": 1,
    "KeyTwo": true,
    "KeyThree": {
      "Message": "Oh, that's nice..."
    }
  }
}

Next, let's create a few classes that represent these configuration settings anywhere in the project:

public class Settings
{
	public int KeyOne { get; set; }
	public bool KeyTwo { get; set; }
	public NestedSettings KeyThree { get; set; } = null!;
}

public class NestedSettings
{
	public string Message { get; set; } = null!;
}

We will now need to configure our json file with a few Microsoft.Extensions.Configuration NuGet packages. Added the following into your .csproj:

<ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
</ItemGroup>

Now, let's read in our json file, and add it to our builder.Configuration in our CreateMauiApp method after we create the builder:

var a = Assembly.GetExecutingAssembly();
using var stream = a.GetManifestResourceStream("MauiApp27.appsettings.json");

var config = new ConfigurationBuilder()
			.AddJsonStream(stream)
			.Build();

builder.Configuration.AddConfiguration(config);

The code above reads in the manifest resource from the assembly into a stream. You will want to change the MauiApp27 to whatever your assembly name is for your project. After that we load it up and add our new configuration to the builder's main ConfigurationManager. We do this so we can dependency inject our IConfiguration anywhere we want.

Note here that there will be some perf here for reading it in from the file. You should consider if you really need to use appsettings.json as your json is never going to change once compiled up.

We will want to add our MainPage as a Transient into our IServiceCollection in our CreateMauiApp method:

 builder.Services.AddTransient<MainPage>();

Now we can adjust our App.xaml.cs to inject our MainPage:

	public App(MainPage page)
	{
		InitializeComponent();

		MainPage = page;
	}

Then over on our MainPage, we can inject our IConfiguration and then use it when our button is clicked to read in our Settings and bind them together:

	int count = 0;
	IConfiguration configuration;
	public MainPage(IConfiguration config)
	{
		InitializeComponent();

		configuration = config;

	}

	private async void OnCounterClicked(object sender, EventArgs e)
	{
		count++;
		CounterLabel.Text = $"Current count: {count}";

		SemanticScreenReader.Announce(CounterLabel.Text);

		var settings = configuration.GetRequiredSection("Settings").Get<Settings>();
		await DisplayAlert("Config", $"{nameof(settings.KeyOne)}: {settings.KeyOne}" +
            $"{nameof(settings.KeyTwo)}: {settings.KeyTwo}" +
            $"{nameof(settings.KeyThree.Message)}: {settings.KeyThree.Message}", "OK");
	}

Now, if you aren't using constructor injection don't worry as you can still access it manually by exposing the IServiceProvider in our MauiProgram.cs:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        //normal builder stuff

        var app = builder.Build();
        Services = app.Services;

        return app;
    }

    public static IServiceProvider Services { get; private set; }
}

Then you can access it via: MauiProgram.Services.GetService();

Either way, you are good to go and you have access to these settings!!!

Want to learn more? Be sure to read the full documentation on Configuration in .NET. Full source code example can be found on GitHub right here!

Want to go farther? Check out Allan Ritchie's awesome Shiny Extensions library at https://github.com/shinyorg/configurationextensions

Checkout my videos on YouTube

]]>
Implementing In-App Subscriptions in iOS & Android with no backend servers https://staging.montemagno.com/ios-android-subscription-implemenation-strategies/ 62014fb35f3a88004810373c Mon, 07 Feb 2022 21:00:17 GMT

Over the years I have created several apps that I am super proud of, and for the most part, I have always put them out for free onto the app stores. More recently I have been experimenting with different monetization strategies for the apps so users can unlock features or just leave a tip. For the longest time I kept things simple with a "non-consumable" one-time purchase. The strategy is straight forward, they either purchased the item or they didn't. Based on this information you would unlock the features in the app. My nifty InAppBilling .NET plugin for iOS, Android, macOS, and Windows comes in super handy and makes it just a few lines of code to implement this logic. Over the holiday break I decided to take things a step further and dip my toes into the world of subscriptions, and what I found left my code in a complete state of madness! Before I go into details on how I decided to implement subscriptions, I recommend listening to our recent Merge Conflict episode all about subscriptions!

One of my goals of this was to not use any backend servers or accounts at all as I didn't want to add additional costs to management. That leads to even more complexity, but for me it was worth it, so let's get into it!

Setting up subscriptions

The first thing that you are going to need to do is setup your subscriptions in the App Store and Google Play. Each store has the concept of "Groups" that have multiple subscription options in them that users can upgrade/downgrade from. I am just offering a single subscription to keep things simple. The settings are straight forward:

  • How long is a subscription (1 week, 1 month, 2 months, 3 months, 6 months, 1 year)
  • How much is the subscription (as low as 0.49)
  • Name, Description, and Product ID

It is important you select the correct subscription length as you can't change this after it is approved. Additionally, you should set a unique Product ID across your apps and try to name them the same between iOS and Android for simplicity later. You can change the price later if you desire, but note that lowering it will affect all users, but if you increase you have the option to make it for all users or just for new subscriptions. I prefer to make it a bit higher to start and then adjust later as needed.

Apple and Google have great documentation if you need more information.

Purchasing subscriptions

Now that we have setup the subscription information, we can start our implementation. I mean technically you could skip the step ahead as well if you haven't prepped the app store yet, but of course you won't be able to test out things.

Purchase screen for subscription
Purchase screen for subscription

Purchasing is straight forward, and for this blog I am going to use my InAppBilling plugin that under the hood uses StoreKit and the Android Billing Library. Making the purchase is the same as any other purchase using the SDK, but the information that is returned is the most important part.

async Task PurchaseSubscription()
{
    if (IsBusy)
        return;

    IsBusy = true;
    try
    {

        // check internet first with Essentials
        if (Connectivity.NetworkAccess != NetworkAccess.Internet)
            return;

        // connect to the app store api
        var connected = await CrossInAppBilling.Current.ConnectAsync();
        if (!connected)
            return;

        var productIdSub = "mysubscriptionid";

        //try to make purchase, this will return a purchase, empty, or throw an exception
        var purchase = await CrossInAppBilling.Current.PurchaseAsync(productIdSub, ItemType.Subscription);

        if (purchase == null)
        {
            //nothing was purchased
            return;
        }

        if (purchase.State == PurchaseState.Purchased)
        {
            Settings.SubExpirationDate = DateTime.UtcNow.AddSubTime();
            Settings.HasPurchasedSub = true;
            Settings.CheckSubStatus = true;

            // Update UI if necessary if they have
            SetPro();

            try
            {
                // It is required to acknowledge the purchase, else it will be refunded
                if (DeviceInfo.Platform == DevicePlatform.Android)
                    await CrossInAppBilling.Current.AcknowledgePurchaseAsync(purchase.PurchaseToken);
            }
            catch (Exception ex)
            {
                Logger.AppendLine("Unable to acknowledge purcahse: " + ex);
            }
        }
        else
        {
            throw new InAppBillingPurchaseException(PurchaseError.GeneralError);
        }
    }
    catch (InAppBillingPurchaseException purchaseEx)
    {
        // Handle all the different error codes that can occure and do a pop up
    }
    catch (Exception ex)
    {
        // Handle a generic exception as something really went wrong
    }
    finally
    {
        await CrossInAppBilling.Current.DisconnectAsync();
        IsBusy = false;

    }
}

This looks like a lot of code, but it is only four calls to the billing service itself: Connect, buy, acknowledge (on Android), disconnect. And funny enough, on iOS you don't even need to really connect or disconnect as it doesn't do anything.

Now, the real thing to pay attention to is what we do when the user makes a successful purchase. I store 3 values that I store in preferences using Essentials:

   Settings.SubExpirationDate = DateTime.UtcNow.AddSubTime();
   Settings.HasPurchasedSub = true;
   Settings.CheckSubStatus = true;
  • SubExpirationDate: This value lets me check if the subscription is still valid and when to refresh the subscription status next.
  • HasPurchasedSub: A simple value to determine if they have EVER purchased a subscription. Just valuable information that I will use later.
  • CheckSubStatus: This value I use to determine if I have prompted the user to refresh the subscription status, it is true if the subscription is valid, and I have never prompted them.

That AddSubTime is a nice little extension method I made throughout my app to make it easier when checking if subscriptions are valid or not.

public static DateTime AddSubTime(this DateTime dateTime)
            => dateTime.AddMonths(1).AddDays(5);

I decided to add not only a month, but also five extra days just in case there is some weird billing issue. I figure what an extra five days anyway.

Now throughout my app I can run a single check to see if they have purchased a subscription and if it is valid:

public static bool IsSubValid => HasPurchasedSub && SubExpirationDate > DateTime.UtcNow;

Handling subscription renewals and cancelations

At this point, the user now has access to the subscription for a full month (and 5 days)! What happens when it is time to refresh that subscription status? I went with a simple approach on launch of the application that notifies the user that the subscription status needs to be refreshed. I could do this in the background, but I made it very transparent to the users.

if(Settings.CheckSubStatus && Settings.HasPurchasedSub && !Settings.IsSubValid)
{
    Settings.CheckSubStatus = false;
    if(await DisplayAlert("Subcription Status Refresh", "Looks like it is time to update your subscription status. If you canceled and resumed, you can always refresh your status on the settings page by restoring purchases.", "Refresh status", "Maybe Later"))
    {
        if (Connectivity.NetworkAccess != NetworkAccess.Internet)
        {
            Settings.CheckSubStatus = true;
            await DisplayAlert("No internet", "Please check internet connection and try restore purchases again in settings.", "OK");
            return;
        }
        //refresh status here
        await RestorePurchases();
    }
}

Here, I check to see if they ever purchased a subscription, if it is no longer valid, and if I should check the subcription status with a prompt. If they want to refresh the status, I call the RestorePurchase method.

Now, this is where things get FUNKY and weird because iOS and Android differ so much.

  • iOS: returns every single transaction including the original purchase and all renewals with proper transaction date time stamps!
  • Android: Only returns current valid subscriptions and the time stamp is always the original purchase date of the subscription, not the renewals.

I personally prefer the iOS mechanism for this as it makes the code much cleaner. All you must  do is order the purchases descending by transaction date and compare dates to see if a subscription is valid. On Android, if it returns a subscription then it is valid and you must figure out the expiration date manually by adding time over and over to the original transaction date, which seems problematic. However, after much testing, here is what I came up with to check to see if the subscription has been renewed:

async Task RestorePurchases()
{
    if (IsBusy)
        return;
    IsBusy = true;

    try
    {

        var connected = await CrossInAppBilling.Current.ConnectAsync();
        if (!connected)
            return;

        var foundStuff = false;

        var subs = await CrossInAppBilling.Current.GetPurchasesAsync(ItemType.Subscription);

        if (subs?.Any(p => p.ProductId == productIdSub) ?? false)
        {
            var sorted = subs.Where(p => p.ProductId == productIdSub).OrderByDescending(i => i.TransactionDateUtc).ToList();
            var recentSub = sorted[0];
            if (recentSub != null)
            {
                // On Android as long as you have one here then it is valid subscription
                if (DeviceInfo.Platform == DevicePlatform.Android)
                {
                    foundStuff = true;
                    Settings.HasTippedSub = true;
                    Settings.CheckSubStatus = true;

                    //loop through transactions and keep adding a month until valid
                    var date = recentSub.TransactionDateUtc;
                    while (date < DateTime.UtcNow)
                        date = date.AddMonths(1);

                    Settings.SubExpirationDate = date.AddDays(5);

                    SetPro();
                }
                else
                {
                    if (recentSub.TransactionDateUtc.AddSubTime() > DateTime.UtcNow)
                    {
                        foundStuff = true;
                        Settings.HasTippedSub = true;
                        Settings.CheckSubStatus = true;
                        Settings.SubExpirationDate = recentSub.TransactionDateUtc.AddSubTime();
                        SetPro();
                    }
                }

                if (DeviceInfo.Platform == DevicePlatform.Android && !recentSub.IsAcknowledged)
                {
                    try
                    {
                        await CrossInAppBilling.Current.AcknowledgePurchaseAsync(recentSub.PurchaseToken);
                    }
                    catch (Exception ex)
                    {
                        Logger.AppendLine("Unable to acknowledge purchase: " + ex);
                    }
                }
            }
        }

        if (!foundStuff)
        {
            await DisplayAlert("Hmmmm!", $"Looks like we couldn't find any subscription renewals, check your purchases and restore them in settings. Don't worry, all of your ride data will be saved.", "OK");
        }
        else
        {
            await DisplayAlert("Status Refreshed!", $"Thanks for being awesome and subscribing for another month of My Cadence Pro!", "OK");
        }

    }
    catch (Exception ex)
    {
        Debug.WriteLine("Issue connecting: " + ex);
        await DisplayAlert("Uh Oh!", $"Looks like something has gone wrong, please check connection and restore in the settings.  Code: {ex.Message}", "OK");
    }
    finally
    {
        IsBusy = false;
        SetPro();
        await CrossInAppBilling.Current.DisconnectAsync();
    }
}

It is not pretty, but totally working well. Note that for iOS I take the most recent subscription, add time, and compare it with the current UTC time: recentSub.TransactionDateUtc.AddSubTime() > DateTime.UtcNow and if it is still valid then I update the expiration date.

Verifying & restoring a subscription

One important feature that your app must implement is a "Restore Purchases" feature for in-app purchases and subscriptions. This is important if your user gets a new device, has multiple devices, or uninstalls and re-installs the app. I add a button directly on the settings/upgrade screen to do this. The application logic is actually the same as the renewal check above! Code re-use to the max!!! You may have other in-app purchases that you may want to put in there as well, so be sure to restore all your purchases!

Receipt validation

At this point we have finished all the in-app code for subscriptions, renewals, and restoration. See, it wasn't that bad :).... well except that we haven't done any receipt validation, which means the user could have cancelled, asked for a refund, paused a subscription, or who knows what else. Oh, and receipt validation also ensures that users haven't hacked your app in some way to intercept the calls to get free stuff. Look how easy this looks:

Receipt validation is super important, and I recommend you checkout my documentation with tons of links to other important resources including how to use Azure Functions as a receipt validation service. For this blog, and in my apps currently I am not doing receipt validation. I haven't personally had any issues, but my apps are only downloaded a few thousand times, not millions of times. So, I am going to leave this up to you based on your application.

Testing subscriptions

Now is the fun part of testing!!!!!!!

This is the worst part because when you test everything is different in a sandbox, and each platform is different. I ended up using TestFlight and Google Play Internal rings for testing with my normal account. When you subscribe on iOS and Android you aren't charged anything during testing, but they do renew differently for testing purposes. Android will just continue to renew every 5 minutes (for a 1 month sub, longer for others) and then you have to manually cancel the subscription. I like this approach a lot as it doesn't clutter up all my transactions and lets me do more realistic world testing. iOS is a bit of an interesting one as it also will renew the subscription every 5 minutes but will do it up to 12 times and then stop renewing. This is a fine approach, but I just wish they worked the same.

I could write for days on testing and all the complexity, but there is an amazing blog for iOS testing from RevenueCat that saved my life and outlines everything you need to know. Google actually has pretty good documenation for this and really helped me get through the process. Take your time on testing and make sure to test all of the different scenarios. One pro tip that I can offer is to change the amount of time for a valid subscription to 5 minutes (instead of my 1 month & 5 days) so you can test out the duration that Apple/Google renew on. Once you have validated that you can change it back and ship to the store.

In-App Subscriptions SaaS... RevenueCat

Implementing in-app subscriptions can be extremely complicated and can get even more complicated when you throw in user accounts, third party integrations, and subscriptions on the web. After reading the testing blog on RevenueCat, I realized that it was an entire SaaS solution for in-app subscriptions and is genius.

If you are really looking for a full solution this may be a good option for you to investigate. I have still decided to roll my own and use the raw APIs of the platform, but I have to say RevenueCat is pretty awesome and just shows how complex these APIs and testing matrixes are.

Other Considerations

In-app subscriptions are pretty neat as they offer a lot of flexibility over a single IAP or up-front app purchase. For example you can:

  • Offer a free trial for a month
  • Offer discounted rate for the first X months
  • Offer discounts for longer subscriptions
  • Promote them in the app store as another entry (iOS)

It is true that we are becoming a subscription based society and some people hate subscriptions. However, if you have great features and want to help your long-term development it is a great option to generate revenue. One option that I have in my apps is the option to do a "life time" subscription purchase. This is actually not a subscription, but a non-consumable in-app purchase. This adds a bit of complexity to the mix, but I think it is worth it at the end of the day. I hope that you found this blog a bit interesting and helpful on your IAP journey!

Checkout my videos on YouTube

]]>
Goodbye Android Emulators, the Windows Subsystem for Android is Here! https://staging.montemagno.com/goodbye-android-emulators-windows-subsytem-for-android-is-here/ 6172b75df2a01a003b1a2b76 Fri, 22 Oct 2021 17:39:00 GMT

Android Emulators have been a pain in everyone's side for pretty much ever. Intel HAXM is great, but limited to Intel, then there were custom emulators like Xamarin Android Player and Genymotion, but required additional installs, and finally Hyper-V support, which brought with it AMD support, and side by side Docker support as well. But now, Windows 11 is here and so is a brand new Insiders Preview that brings with it Android app support, which is CRAZY AWESOME. More so is that you can use the new Windows Subsystem for Android to develop, debug, and deploy your Android apps from your favorite IDE! Let's walk through what you need to do to get it all setup. However, if you want to just watch a video I have that for you as well!

Windows Subsystem for Android

So, what is this thing? Well, it is like the Windows Subsystem for Linux enabling you to have a full subsystem available for Android instead of Linux. Now at this point, you don't have access to 100% Android. It is not a device just sitting there, but instead think of Windows being the OS that can run Android apps. There is no Android launcher because Windows is the launcher of the apps.

Get Windows Insiders Beta

The first thing that you will need is the Windows Insiders Beta build (not Dev) that matches Windows 11 Build 22000.xxx. You can read all about how to turn on Windows Insiders on their blog.

Windows 11 21H21
Windows 11 21H21

Install Windows Subsystem for Android

Now it is time to install the Windows Subsystem for Android, which also comes with the Amazon App Store. This is done directly from the Microsoft Store on Windows and will require a reboot and some final installation steps.

App Store Listing for WSA
App Store Listing for WSA

Once it is installed you will want to search and pin the Windows Subsystem for Android app.

Search for Android
Search for Android

Developer Mode Setup

Inside of the WSA app you will find all sorts of great options, but a few things to turn on are "Continuous" mode, so the subsystem is always ready to go.

Then, flip on Developer mode:

Notice here that once you turn on developer mode you get instructions on how to connect to adb.

ADB Connect

I am going to steal this part directly from the documentation. To connect to the Windows Subsystem for Android VM for debugging, you have two options:

Recommended Method:

  • Use localhost for connecting to debugging. The IP address of the localhost is: 127.0.0.1:58526. Windows Subsystem for Android must be running in order to connect, the best way to launch Windows Subsystem for Android is by launching an Android app that was installed with the Amazon Appstore.
  • To connect to the localhost address of Windows Subsystem for Android, enter: adb connect 127.0.0.1:58526

Alternative Method: Use the Windows Subsystem for Android Settings app to get the IP address.

  • Launch the Settings app. (Use Windows Search to select and launch.)
  • The IP address will be displayed under the IP address section. If there is no IP address being displayed, launch an Android app that was installed using the Amazon Appstore, then select Refresh on the IP address button in the Settings app.

Now that you have the IP address to connect to the Windows Subsystem for Android VM, connect using adb connect: adb connect 172.22.137.166

Originally, I had to connect through the IP address, however after a reboot and relaunching WSA, I got a firewall prompt and then 127.0.0.1 started working for me.

Debug!

Now that you are connected via ADB, WSA will just show up in your IDE of choice because it is a device!

Debug away!

Your Apps are on Windows

That is right, your apps are on Windows! You don't need to boot things up to see your app because they are literally in the start menu!

Also, when you open the app setting, they are the Android app settings! How cool is that!?!?!

Limitations

There are a few things to be aware of with WSA. There is no Google Play Services, so if you are testing out maps or other services that is not going to work. It isn't an emulator, which means you don't get to control the skin or conditions under where your app runs since it is running under WSA, not an emulator. Not every sensor is connected, for example it looks like Bluetooth isn't forwarded to WSA from your Windows machine. I also haven't found a way for apps running under WSA to talk about to the local machine for debugging web servers, but I am sure it is a matter of time before someone figures it out.

However, this is the very first preview of WSA and I am extremely happy with it! I love it and never want to open an Android emulator ever again!

Checkout my videos on YouTube

]]>