Skip to content

feat: add in-app sponsorship prompts and reminders#28

Merged
wiiiimm merged 2 commits into
mainfrom
feature/sponsor-message
Sep 6, 2025
Merged

feat: add in-app sponsorship prompts and reminders#28
wiiiimm merged 2 commits into
mainfrom
feature/sponsor-message

Conversation

@wiiiimm
Copy link
Copy Markdown
Owner

@wiiiimm wiiiimm commented Sep 6, 2025

Summary

This PR adds sponsorship prompts throughout the gh-manager-cli app to encourage users to support the project.

Features Added

1. Post-Exit Sponsor Message

  • Shows a thank you message when the TUI exits normally
  • Includes links to Buy Me a Coffee, wiiiimm.codes, and GitHub repo
  • Only shows on normal exit (not for --help or --version flags)

2. Info Modal Sponsor Section

  • Added yellow-bordered sponsor box in repository info modal (I key)
  • Shows "💚 Enjoying gh-manager-cli?" with Buy Me a Coffee link

3. Help Footer Sponsor Line

  • Added "💚 Support: buymeacoffee.com/wiiiimm" to the help footer

4. Periodic Sponsor Reminder

  • Tracks successful archive/delete operations
  • Shows reminder notification after every 5 operations
  • Auto-hides after 5 seconds
  • Non-intrusive yellow-bordered notification with:
    • "💚 Thanks for using gh-manager-cli!"
    • "Your support helps craft more open-source tools"
    • "☕ buymeacoffee.com/wiiiimm"

Testing

  • ✅ Post-exit message tested with normal exit
  • ✅ Verified --help and --version don't show sponsor message
  • ✅ Info modal sponsor section visible
  • ✅ Help footer includes sponsor line
  • ✅ Periodic reminder triggers after 5 operations

Implementation Details

  • Added operation tracking state in RepoList component
  • Sponsor reminder uses React state with setTimeout for auto-hide
  • All sponsor messages use consistent branding and colors
  • Links point to buymeacoffee.com/wiiiimm

Summary by CodeRabbit

  • New Features
    • Sponsor reminders in Repo List: shows a prominent banner after every 5 successful archive/delete actions; auto-hides after 5 seconds.
    • Info modal now includes a support section with a donation link (Buy Me a Coffee).
    • Console displays a decorative sponsorship message on normal, interactive exits (not shown for errors or when using help/version flags).
    • Help footer extended with an additional sponsorship line to encourage support.

- Show sponsorship message when TUI exits normally
- Display Buy Me a Coffee link
- Add wiiiimm.codes site link
- Invite users to leave feedback on GitHub
- Only shows on normal exit, not for --help or --version
- Add sponsor section to InfoModal (yellow border box)
- Add sponsor line to help footer
- Add periodic sponsor reminder after every 5 operations
- Reminder auto-hides after 5 seconds
- Shows buymeacoffee.com/wiiiimm link
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 6, 2025

Walkthrough

Adds sponsorship messaging across CLI and UI: a console banner on normal interactive exits, a donation block in InfoModal, and a reminder system in RepoList that shows a banner after every five successful archive/delete operations, auto-hiding after 5 seconds. Help footer gains a sponsorship line.

Changes

Cohort / File(s) Summary
CLI exit sponsorship message
src/index.tsx
Adds showSponsorshipMessage and invokes it on process exit only when code===0 and not invoked with --version/--help; existing shutdown/logging unchanged.
Info modal promo block
src/ui/components/modals/InfoModal.tsx
Inserts a yellow-bordered Box with three Text lines promoting donations; purely presentational; no control-flow changes.
RepoList sponsor reminder system
src/ui/views/RepoList.tsx
Introduces state (operationCount, showSponsorReminder) and trackSuccessfulOperation to trigger a banner every 5 successful ops; instruments archive/delete success paths; renders banners in header and main; extends help footer with a sponsorship line.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant CLI as CLI Process
  participant Exit as process.on('exit')
  participant Msg as showSponsorshipMessage

  User->>CLI: Run command
  CLI-->>Exit: Exit event (code)
  Exit->>Exit: Compute isNormalExit (code===0)<br/>and isInteractiveSession (!--version/--help)
  alt Normal interactive exit
    Exit->>Msg: showSponsorshipMessage()
    Msg-->>Exit: Printed banner
  else Non-zero or non-interactive
    Exit-->>Exit: Skip banner
  end
  Exit-->>CLI: Continue existing shutdown/logging
Loading
sequenceDiagram
  autonumber
  participant User
  participant UI as RepoList
  participant Ops as Archive/Delete Ops
  participant Tracker as trackSuccessfulOperation
  participant Banner as Sponsor Reminder

  User->>UI: Archive/Delete action
  UI->>Ops: Perform operation
  Ops-->>UI: Success
  UI->>Tracker: Increment operationCount
  alt Every 5th success
    Tracker->>UI: set showSponsorReminder = true
    UI->>Banner: Render banner (header & main)
    Note over UI,Banner: Auto-hide after 5 seconds
  else Not a 5th success
    Tracker-->>UI: No banner
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I thump my paws—five hops, a cheer,
A banner blinks, “Buy me a coffee!” here. ☕🐇
On gentle exits, whispers glow,
In modals, sunny borders show.
Code nibble, ship nibble—happily,
Sponsors help this burrow be.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/sponsor-message

Comment @coderabbitai help to get the list of available commands and usage tips.

@wiiiimm wiiiimm merged commit 443aa4b into main Sep 6, 2025
1 of 3 checks passed
Comment thread src/ui/views/RepoList.tsx
setShowSponsorReminder(true);
// Hide the reminder after 5 seconds
setTimeout(() => setShowSponsorReminder(false), 5000);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unmanaged Timers Cause React Warnings and Memory Leaks

The setTimeout in trackSuccessfulOperation isn't managed, which can lead to React warnings and memory leaks if the component unmounts. Additionally, multiple operations in quick succession can cause the sponsor reminder to disappear prematurely due to overlapping timers.

Fix in Cursor Fix in Web

Comment thread src/ui/views/RepoList.tsx
<Text color="yellow" dimColor={modalOpen ? true : undefined}>
💚 Support the project: buymeacoffee.com/wiiiimm
</Text>
</Box>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Footer Height Mismatch Causes UI Issues

The help footer now renders 5 lines (including the new sponsorship line), but the footerHeight constant used for layout calculations remains at 4. This mismatch causes incorrect content area height calculations, potentially leading to UI overflow or content being cut off.

Fix in Cursor Fix in Web

wiiiimm pushed a commit that referenced this pull request Sep 6, 2025
# [1.34.0](v1.33.0...v1.34.0) (2025-09-06)

### Features

* add in-app sponsorship prompts and reminders ([#28](#28)) ([443aa4b](443aa4b))
@wiiiimm
Copy link
Copy Markdown
Owner Author

wiiiimm commented Sep 6, 2025

🎉 This PR is included in version 1.34.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/ui/components/modals/InfoModal.tsx (1)

54-58: Prefer chalk-coloured single Text and a fully qualified URL

Avoid Ink colour props in src/ui/**; pre-colour with chalk and render inside a single Text. Also use https so terminals detect a clickable link.

-      <Box flexDirection="column" borderStyle="single" borderColor="yellow" paddingX={1} marginY={1}>
-        <Text color="yellow">💚 Enjoying gh-manager-cli?</Text>
-        <Text color="gray">Support the project at:</Text>
-        <Text color="cyan">buymeacoffee.com/wiiiimm</Text>
-      </Box>
+      <Box flexDirection="column" borderStyle="single" borderColor="yellow" paddingX={1} marginY={1}>
+        <Text>
+          {chalk.yellow('💚 Enjoying gh-manager-cli?')}
+          {'\n'}
+          {chalk.gray('Support the project at:')}
+          {'\n'}
+          {chalk.cyan('https://buymeacoffee.com/wiiiimm')}
+        </Text>
+      </Box>
src/index.tsx (1)

102-113: Gate the exit banner to interactive TTY sessions and allow opt-out

Avoid printing in CI/non-TTY contexts and provide an env opt-out. Also safer to check isTTY in addition to flags.

-  const isNormalExit = code === 0;
-  const isInteractiveSession = !argv.includes('--version') && 
-                               !argv.includes('-v') && 
-                               !argv.includes('--help') && 
-                               !argv.includes('-h');
-  
-  if (isNormalExit && isInteractiveSession) {
+  const isNormalExit = code === 0;
+  const isInteractiveSession = !argv.includes('--version') &&
+                               !argv.includes('-v') &&
+                               !argv.includes('--help') &&
+                               !argv.includes('-h');
+  const isTTY = Boolean(process.stdout && process.stdout.isTTY);
+  const sponsorOptOut = process.env.GH_MANAGER_NO_SPONSOR === '1' || process.env.CI === 'true';
+
+  if (isNormalExit && isInteractiveSession && isTTY && !sponsorOptOut) {
     showSponsorshipMessage();
   }

Optional: prefer process.stdout.write over console.log inside the exit handler to minimise buffering risk.

src/ui/views/RepoList.tsx (1)

2229-2233: Use chalk and a fully qualified link for the sponsorship footer line

Pre-colour with chalk and include https for better terminal linkifying.

-          <Text color="yellow" dimColor={modalOpen ? true : undefined}>
-            💚 Support the project: buymeacoffee.com/wiiiimm
-          </Text>
+          <Text dimColor={modalOpen ? true : undefined}>
+            {chalk.yellow('💚 Support the project: https://buymeacoffee.com/wiiiimm')}
+          </Text>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b975e26 and fec84ee.

📒 Files selected for processing (3)
  • src/index.tsx (1 hunks)
  • src/ui/components/modals/InfoModal.tsx (1 hunks)
  • src/ui/views/RepoList.tsx (7 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use strict TypeScript with comprehensive types; avoid any without clear justification

Files:

  • src/ui/components/modals/InfoModal.tsx
  • src/ui/views/RepoList.tsx
  • src/index.tsx
src/ui/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

src/ui/**/*.tsx: Implement React/Ink UI as functional components using hooks
Use chalk for colours instead of Ink colour props to avoid nested Text issues
Pre-colour strings and render within a single to prevent nested Text rendering issues
Use spacers for consistent terminal spacing

Files:

  • src/ui/components/modals/InfoModal.tsx
  • src/ui/views/RepoList.tsx
src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Use British English for all user-facing text (e.g., organisation, authorisation, colour)

Files:

  • src/ui/components/modals/InfoModal.tsx
  • src/ui/views/RepoList.tsx
  • src/index.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-05T11:52:17.573Z
Learnt from: CR
PR: wiiiimm/gh-manager-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-09-05T11:52:17.573Z
Learning: Applies to src/ui/**/*.tsx : Use <Box minHeight={...}> spacers for consistent terminal spacing

Applied to files:

  • src/ui/views/RepoList.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (4)
src/index.tsx (1)

85-97: LGTM: clear, friendly exit banner function

Function is self-contained and readable.

src/ui/views/RepoList.tsx (3)

96-99: LGTM: state for sponsor reminder

Looks good and typed properly.


321-321: Correct placement of tracking call

Called only after successful archive/unarchive updates; that’s the right spot.


532-532: Correct placement of tracking call

Called only after successful delete; that’s the right spot.

Comment thread src/ui/views/RepoList.tsx
Comment on lines +196 to +208
// Helper to track successful operations and show sponsor reminder
function trackSuccessfulOperation() {
const newCount = operationCount + 1;
setOperationCount(newCount);

// Show sponsor reminder every 5 operations
if (newCount % 5 === 0) {
setShowSponsorReminder(true);
// Hide the reminder after 5 seconds
setTimeout(() => setShowSponsorReminder(false), 5000);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make the counter update atomic and clear the hide timer to avoid leaks

Use functional setState to prevent stale reads, and store/clear the timeout so it doesn’t fire after unmount or stack with multiple operations.

-  function trackSuccessfulOperation() {
-    const newCount = operationCount + 1;
-    setOperationCount(newCount);
-    
-    // Show sponsor reminder every 5 operations
-    if (newCount % 5 === 0) {
-      setShowSponsorReminder(true);
-      // Hide the reminder after 5 seconds
-      setTimeout(() => setShowSponsorReminder(false), 5000);
-    }
-  }
+  function trackSuccessfulOperation() {
+    setOperationCount(prev => {
+      const next = prev + 1;
+      if (next % 5 === 0) {
+        setShowSponsorReminder(true);
+        // Reset any existing timer and start a new one
+        if (sponsorTimerRef.current) clearTimeout(sponsorTimerRef.current);
+        sponsorTimerRef.current = setTimeout(() => setShowSponsorReminder(false), 5000);
+      }
+      return next;
+    });
+  }

Additions needed outside this hunk:

// near other refs (e.g., around the copyToastTimerRef)
const sponsorTimerRef = useRef<NodeJS.Timeout | null>(null);

// extend existing unmount cleanup
useEffect(() => {
  return () => {
    if (copyToastTimerRef.current) clearTimeout(copyToastTimerRef.current);
    if (sponsorTimerRef.current) clearTimeout(sponsorTimerRef.current);
  };
}, []);
🤖 Prompt for AI Agents
In src/ui/views/RepoList.tsx around lines 196 to 208, the operation counter uses
non-functional state updates and the sponsor reminder timeout is not tracked or
cleared; change setOperationCount to the functional form (prev => prev + 1) to
avoid stale reads, store the timeout ID in a new sponsorTimerRef (declare const
sponsorTimerRef = useRef<NodeJS.Timeout | null>(null) near other refs), clear
any existing sponsorTimerRef.current before creating a new setTimeout and assign
the ID to sponsorTimerRef.current, and extend the component unmount cleanup
effect to clear sponsorTimerRef.current if set so the timeout won't fire after
unmount or stack across operations.

Comment thread src/ui/views/RepoList.tsx
Comment on lines +1643 to +1654
{/* Sponsor reminder notification */}
{showSponsorReminder && (
<Box marginX={1} marginBottom={1}>
<Box borderStyle="single" borderColor="yellow" paddingX={2} paddingY={1}>
<Box flexDirection="column" alignItems="center">
<Text color="yellow">💚 Thanks for using gh-manager-cli!</Text>
<Text color="gray">Your support helps craft more open-source tools</Text>
<Text color="cyan">☕ buymeacoffee.com/wiiiimm</Text>
</Box>
</Box>
</Box>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use chalk for colouring and adjust layout to account for the banner’s height

  • Colour: follow src/ui/** guideline to pre-colour with chalk in a single Text. Use full https link for clickability.
  • Layout: the banner consumes vertical rows but contentHeight doesn’t subtract it, so the list/footer can be clipped. Subtract the banner height when visible.
-      {showSponsorReminder && (
-        <Box marginX={1} marginBottom={1}>
-          <Box borderStyle="single" borderColor="yellow" paddingX={2} paddingY={1}>
-            <Box flexDirection="column" alignItems="center">
-              <Text color="yellow">💚 Thanks for using gh-manager-cli!</Text>
-              <Text color="gray">Your support helps craft more open-source tools</Text>
-              <Text color="cyan">☕ buymeacoffee.com/wiiiimm</Text>
-            </Box>
-          </Box>
-        </Box>
-      )}
+      {showSponsorReminder && (
+        <Box marginX={1} marginBottom={1}>
+          <Box borderStyle="single" borderColor="yellow" paddingX={2} paddingY={1}>
+            <Text>
+              {chalk.yellow('💚 Thanks for using gh-manager-cli!')}{'\n'}
+              {chalk.gray('Your support helps craft more open‑source tools')}{'\n'}
+              {chalk.cyan('☕ https://buymeacoffee.com/wiiiimm')}
+            </Text>
+          </Box>
+        </Box>
+      )}

Add this outside to fix height:

// after footer/header constants
const sponsorHeight = showSponsorReminder ? 4 : 0;
// subtract sponsorHeight
const contentHeight = Math.max(1, availableHeight - headerHeight - footerHeight - containerPadding - sponsorHeight);

Optional: centralise the sponsor URL/message in a constants module to avoid drift across files.

🤖 Prompt for AI Agents
In src/ui/views/RepoList.tsx around lines 1643-1654, the sponsor banner should
use chalk to pre-colour a single Text node and use a full https:// link, and the
UI height calculations must subtract the banner height so list/footer aren’t
clipped; update the banner to render a single Text with chalk-applied colors for
each line (no inline color props), replace the short URL with the full
https://buymeacoffee.com/wiiiimm, and after the header/footer constants add a
sponsorHeight = showSponsorReminder ? 4 : 0 then subtract sponsorHeight from
contentHeight calculation (availableHeight - headerHeight - footerHeight -
containerPadding - sponsorHeight). Optionally move the sponsor message/URL into
a shared constants module to avoid duplication.

Comment thread src/ui/views/RepoList.tsx
</Box>

{/* Help footer - 4 lines */}
{/* Help footer - 5 lines */}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Footer now has 5 lines but footerHeight is still 4 — list area can be clipped

Update the footerHeight constant accordingly (or compute dynamically).

Outside this hunk:

-const footerHeight = 4; // Footer with border + margin (flexible height)
+const footerHeight = 5; // Footer with border + margin (matches 5 rendered lines)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Help footer - 5 lines */}
const footerHeight = 5; // Footer with border + margin (matches 5 rendered lines)
🤖 Prompt for AI Agents
In src/ui/views/RepoList.tsx around line 2202, the footer is rendered with 5
lines but the footerHeight constant is still set to 4, causing the list area to
be clipped; update the footerHeight to match the actual footer line count (set
to 5) or replace the hardcoded constant with a dynamic calculation (measure the
footer element or derive from computed line-height/rows) and propagate that
value to all layout calculations that use footerHeight so the list area is sized
correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant