rails_tipjar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8a942f0f33f3ae7a78eda233a1dff1ce1a883a144ac1cad2a31e9849f212b708
4
+ data.tar.gz: f00214075a3e9008ddc7aa9b85d46caca6a6792ebbc185407c5dae07ccc114f8
5
+ SHA512:
6
+ metadata.gz: 7a437aedf1dd798d8bb7e76d364236f19c80b1ee3295c72df64477cbaa1ed1f4e51f494a32e5a53da8fa86fd42a7376dc7c53110e916fcd24020f9d6c6739819
7
+ data.tar.gz: 38d10e40392878cf99f43ed646a41401630f97340b97145b02b49ce9a48d8b49a716d5c4c2cb4bf1d59d873614cf5f404e4740d894cbb32045ac402d05f7de86
data/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # RailsTipjar
2
+
3
+ A reusable, customizable tip jar feature for Rails applications using Stripe Payment Links. Add a beautiful tip jar to any Rails app with just a few lines of code!
4
+
5
+ ## Features
6
+
7
+ - ๐ŸŽจ Customizable floating button with multiple icon options
8
+ - ๐Ÿ’ณ Stripe Payment Links integration (no backend payment processing needed)
9
+ - ๐Ÿ“ฑ Fully responsive design
10
+ - ๐ŸŽฏ Multiple positioning options
11
+ - ๐ŸŒ™ Light/Dark theme support
12
+ - ๐Ÿ“Š Built-in analytics tracking
13
+ - โšก Stimulus.js powered interactions
14
+ - ๐Ÿ”ง Zero configuration to get started
15
+
16
+ ## Why Stripe Payment Links?
17
+
18
+ - **No payment code** in your Rails apps
19
+ - **PCI compliant** automatically
20
+ - **No monthly fees** - pay only 2.9% + 30ยข per transaction
21
+ - **International support** with multi-currency
22
+ - **Professional checkout** experience
23
+ - **Automatic receipts** and tax handling
24
+
25
+ ## Installation
26
+
27
+ Add this gem to your Rails application's Gemfile:
28
+
29
+ ```ruby
30
+ # From GitHub (recommended during development)
31
+ gem 'rails_tipjar', git: 'https://github.com/justinpaulson/rails_tipjar'
32
+
33
+ # Or from RubyGems (when published)
34
+ gem 'rails_tipjar'
35
+ ```
36
+
37
+ Then execute:
38
+
39
+ ```bash
40
+ bundle install
41
+ rails generate tipjar:install
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ### 1. Create Stripe Payment Links
47
+
48
+ 1. Go to [Stripe Dashboard](https://dashboard.stripe.com/payment-links)
49
+ 2. Create payment links for different tip amounts
50
+ 3. Set them to "Donation" mode
51
+ 4. Copy the generated URLs
52
+
53
+ ### 2. Configure the Gem
54
+
55
+ Edit `config/initializers/tipjar.rb`:
56
+
57
+ ```ruby
58
+ RailsTipjar.configure do |config|
59
+ config.payment_links = {
60
+ small: "https://buy.stripe.com/your-5-dollar-link",
61
+ medium: "https://buy.stripe.com/your-10-dollar-link",
62
+ large: "https://buy.stripe.com/your-25-dollar-link"
63
+ }
64
+
65
+ config.position = :bottom_right
66
+ config.icon = :coffee
67
+ config.button_text = "Buy me a coffee"
68
+ end
69
+ ```
70
+
71
+ ### 3. Add to Your Layout
72
+
73
+ In `app/views/layouts/application.html.erb`:
74
+
75
+ ```erb
76
+ <!DOCTYPE html>
77
+ <html>
78
+ <head>
79
+ <!-- your head content -->
80
+ </head>
81
+ <body>
82
+ <%= yield %>
83
+
84
+ <!-- Add tip jar button -->
85
+ <%= tipjar_button %>
86
+ </body>
87
+ </html>
88
+ ```
89
+
90
+ That's it! You now have a working tip jar on your site.
91
+
92
+ ## Configuration Options
93
+
94
+ ```ruby
95
+ RailsTipjar.configure do |config|
96
+ # Payment Links (required)
97
+ config.payment_links = {
98
+ small: "...",
99
+ medium: "...",
100
+ large: "...",
101
+ custom: "..." # Optional: for custom amounts
102
+ }
103
+
104
+ # Button position
105
+ # Options: :bottom_right, :bottom_left, :top_right, :top_left
106
+ config.position = :bottom_right
107
+
108
+ # Button icon
109
+ # Options: :coffee, :heart, :star, :dollar, or custom SVG string
110
+ config.icon = :coffee
111
+
112
+ # Theme
113
+ # Options: :light, :dark, :auto
114
+ config.theme = :light
115
+
116
+ # Button text (shown on hover)
117
+ config.button_text = "Buy me a coffee"
118
+
119
+ # Modal customization
120
+ config.modal_title = "Support my work"
121
+ config.modal_description = "Your support helps me continue creating."
122
+
123
+ # Tip amounts shown in modal
124
+ config.custom_amounts = [
125
+ { amount: 5, label: "$5", default: false },
126
+ { amount: 10, label: "$10", default: true },
127
+ { amount: 25, label: "$25", default: false },
128
+ { amount: 50, label: "$50", default: false }
129
+ ]
130
+
131
+ # Additional styling
132
+ config.button_class = "custom-class"
133
+ config.z_index = 1000
134
+
135
+ # Analytics
136
+ config.analytics_enabled = true # Works with GA4 and Plausible
137
+ end
138
+ ```
139
+
140
+ ## Usage
141
+
142
+ ### Basic Button
143
+
144
+ ```erb
145
+ <!-- Default button with all config options -->
146
+ <%= tipjar_button %>
147
+
148
+ <!-- Override position for specific button -->
149
+ <%= tipjar_button position: :top_left %>
150
+
151
+ <!-- Add custom CSS classes -->
152
+ <%= tipjar_button class: "my-custom-class" %>
153
+ ```
154
+
155
+ ### Custom Links
156
+
157
+ ```erb
158
+ <!-- Create a custom tip link -->
159
+ <%= tipjar_link "Support this project", amount: :large %>
160
+
161
+ <!-- With custom styling -->
162
+ <%= tipjar_link "Tip $5", amount: :small, class: "btn btn-primary" %>
163
+ ```
164
+
165
+ ### Modal Only
166
+
167
+ ```erb
168
+ <!-- Render just the modal (for custom triggers) -->
169
+ <%= tipjar_modal %>
170
+ ```
171
+
172
+ ## Styling
173
+
174
+ The gem includes default styles that work with most Rails applications. You can override styles by targeting these CSS classes:
175
+
176
+ ```css
177
+ /* Button */
178
+ .tipjar-button { }
179
+ .tipjar-button-icon { }
180
+ .tipjar-button-text { }
181
+
182
+ /* Modal */
183
+ .tipjar-modal { }
184
+ .tipjar-modal-content { }
185
+ .tipjar-modal-header { }
186
+ .tipjar-amount-button { }
187
+ .tipjar-amount-active { }
188
+ .tipjar-submit-button { }
189
+ ```
190
+
191
+ ## Analytics
192
+
193
+ When `analytics_enabled` is true, the following events are tracked:
194
+
195
+ - `tipjar_loaded` - Button loaded on page
196
+ - `tipjar_modal_opened` - User opened the modal
197
+ - `tipjar_amount_selected` - User selected an amount
198
+ - `tipjar_submitted` - User clicked to proceed to payment
199
+
200
+ Works automatically with:
201
+ - Google Analytics 4 (gtag)
202
+ - Plausible Analytics
203
+ - Custom events via `document.addEventListener('tipjar:event', ...)`
204
+
205
+ ## Development
206
+
207
+ After checking out the repo:
208
+
209
+ ```bash
210
+ cd rails_tipjar
211
+ bundle install
212
+ ```
213
+
214
+ To test in a Rails app, add to your Gemfile:
215
+
216
+ ```ruby
217
+ gem 'rails_tipjar', path: '../path/to/rails_tipjar'
218
+ ```
219
+
220
+ ## Troubleshooting
221
+
222
+ ### Button not appearing
223
+
224
+ 1. Check that `<%= tipjar_button %>` is in your layout
225
+ 2. Verify Stimulus is working in your app
226
+ 3. Check browser console for JavaScript errors
227
+
228
+ ### Modal not opening
229
+
230
+ 1. Ensure Stimulus controller is registered
231
+ 2. Check that tipjar.css is being loaded
232
+ 3. Verify no z-index conflicts with your existing styles
233
+
234
+ ### Payment links not working
235
+
236
+ 1. Ensure your Stripe Payment Links are active
237
+ 2. Check that URLs are correctly configured in initializer
238
+ 3. Test links directly in browser
239
+
240
+ ## Contributing
241
+
242
+ Bug reports and pull requests are welcome on GitHub at https://github.com/justinpaulson/rails_tipjar.
243
+
244
+ ## License
245
+
246
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,154 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["button", "modal", "backdrop", "amountButton"]
5
+ static values = {
6
+ paymentLinks: Object,
7
+ theme: String,
8
+ modalTitle: String,
9
+ modalDescription: String,
10
+ customAmounts: Array,
11
+ analyticsEnabled: Boolean
12
+ }
13
+
14
+ connect() {
15
+ this.selectedAmount = null
16
+ this.setupEventListeners()
17
+
18
+ if (this.analyticsEnabledValue) {
19
+ this.trackEvent("tipjar_loaded")
20
+ }
21
+ }
22
+
23
+ setupEventListeners() {
24
+ // Close modal on escape key
25
+ document.addEventListener("keydown", this.handleEscapeKey.bind(this))
26
+
27
+ // Close modal on backdrop click
28
+ if (this.hasBackdropTarget) {
29
+ this.backdropTarget.addEventListener("click", this.closeModal.bind(this))
30
+ }
31
+ }
32
+
33
+ disconnect() {
34
+ document.removeEventListener("keydown", this.handleEscapeKey.bind(this))
35
+ }
36
+
37
+ handleEscapeKey(event) {
38
+ if (event.key === "Escape" && this.hasModalTarget) {
39
+ this.closeModal()
40
+ }
41
+ }
42
+
43
+ openModal(event) {
44
+ event.preventDefault()
45
+
46
+ if (this.hasModalTarget) {
47
+ this.modalTarget.classList.remove("hidden")
48
+ this.modalTarget.classList.add("flex")
49
+ document.body.style.overflow = "hidden"
50
+
51
+ if (this.analyticsEnabledValue) {
52
+ this.trackEvent("tipjar_modal_opened")
53
+ }
54
+ }
55
+ }
56
+
57
+ closeModal() {
58
+ if (this.hasModalTarget) {
59
+ this.modalTarget.classList.add("hidden")
60
+ this.modalTarget.classList.remove("flex")
61
+ document.body.style.overflow = ""
62
+
63
+ if (this.analyticsEnabledValue) {
64
+ this.trackEvent("tipjar_modal_closed")
65
+ }
66
+ }
67
+ }
68
+
69
+ selectAmount(event) {
70
+ const button = event.currentTarget
71
+ const amount = button.dataset.amount
72
+
73
+ // Remove active class from all buttons
74
+ this.amountButtonTargets.forEach(btn => {
75
+ btn.classList.remove("tipjar-amount-active")
76
+ })
77
+
78
+ // Add active class to selected button
79
+ button.classList.add("tipjar-amount-active")
80
+
81
+ this.selectedAmount = amount
82
+
83
+ if (this.analyticsEnabledValue) {
84
+ this.trackEvent("tipjar_amount_selected", { amount })
85
+ }
86
+ }
87
+
88
+ submitTip(event) {
89
+ event.preventDefault()
90
+
91
+ if (!this.selectedAmount) {
92
+ // Find default amount or select first one
93
+ const defaultButton = this.amountButtonTargets.find(btn =>
94
+ btn.dataset.default === "true"
95
+ ) || this.amountButtonTargets[0]
96
+
97
+ if (defaultButton) {
98
+ this.selectedAmount = defaultButton.dataset.amount
99
+ }
100
+ }
101
+
102
+ const paymentLink = this.getPaymentLink(this.selectedAmount)
103
+
104
+ if (paymentLink) {
105
+ if (this.analyticsEnabledValue) {
106
+ this.trackEvent("tipjar_submitted", {
107
+ amount: this.selectedAmount,
108
+ url: paymentLink
109
+ })
110
+ }
111
+
112
+ window.open(paymentLink, "_blank", "noopener,noreferrer")
113
+ this.closeModal()
114
+ } else {
115
+ console.error("No payment link found for amount:", this.selectedAmount)
116
+ }
117
+ }
118
+
119
+ getPaymentLink(amount) {
120
+ // Check if amount is a key in payment links
121
+ if (this.paymentLinksValue[amount]) {
122
+ return this.paymentLinksValue[amount]
123
+ }
124
+
125
+ // Try to match by dollar amount
126
+ const numericAmount = parseInt(amount)
127
+ if (this.paymentLinksValue[`amount_${numericAmount}`]) {
128
+ return this.paymentLinksValue[`amount_${numericAmount}`]
129
+ }
130
+
131
+ // Fallback to medium or first available link
132
+ return this.paymentLinksValue.medium ||
133
+ this.paymentLinksValue.default ||
134
+ Object.values(this.paymentLinksValue)[0]
135
+ }
136
+
137
+ trackEvent(eventName, data = {}) {
138
+ // Google Analytics 4
139
+ if (typeof gtag !== "undefined") {
140
+ gtag("event", eventName, data)
141
+ }
142
+
143
+ // Plausible
144
+ if (typeof plausible !== "undefined") {
145
+ plausible(eventName, { props: data })
146
+ }
147
+
148
+ // Custom tracking
149
+ const event = new CustomEvent("tipjar:event", {
150
+ detail: { name: eventName, ...data }
151
+ })
152
+ document.dispatchEvent(event)
153
+ }
154
+ }
@@ -0,0 +1,213 @@
1
+ /* Tip Jar Button Styles */
2
+ .tipjar-button {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 0.5rem;
6
+ padding: 0.75rem 1rem;
7
+ background-color: #3b82f6;
8
+ color: white;
9
+ border-radius: 9999px;
10
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
11
+ transition: all 0.3s ease;
12
+ cursor: pointer;
13
+ border: none;
14
+ font-size: 0.875rem;
15
+ font-weight: 500;
16
+ animation: tipjar-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
17
+ }
18
+
19
+ .tipjar-button:hover {
20
+ background-color: #2563eb;
21
+ transform: scale(1.05);
22
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
23
+ }
24
+
25
+ .tipjar-button:focus {
26
+ outline: 2px solid transparent;
27
+ outline-offset: 2px;
28
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
29
+ }
30
+
31
+ .tipjar-button-icon {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ width: 1.5rem;
36
+ height: 1.5rem;
37
+ }
38
+
39
+ .tipjar-button-text {
40
+ white-space: nowrap;
41
+ max-width: 0;
42
+ overflow: hidden;
43
+ transition: max-width 0.3s ease;
44
+ }
45
+
46
+ .tipjar-button:hover .tipjar-button-text {
47
+ max-width: 200px;
48
+ margin-left: 0.25rem;
49
+ }
50
+
51
+ /* Animation */
52
+ @keyframes tipjar-pulse {
53
+ 0%, 100% {
54
+ opacity: 1;
55
+ }
56
+ 50% {
57
+ opacity: 0.8;
58
+ }
59
+ }
60
+
61
+ /* Coin falling animation */
62
+ .tipjar-coin-animation {
63
+ animation: tipjar-coin-drop 2s ease-in-out infinite;
64
+ transform-origin: center;
65
+ }
66
+
67
+ @keyframes tipjar-coin-drop {
68
+ 0% {
69
+ transform: translateY(0) rotate(0deg);
70
+ opacity: 0;
71
+ }
72
+ 20% {
73
+ opacity: 1;
74
+ }
75
+ 80% {
76
+ transform: translateY(7px) rotate(180deg);
77
+ opacity: 1;
78
+ }
79
+ 100% {
80
+ transform: translateY(9px) rotate(180deg);
81
+ opacity: 0;
82
+ }
83
+ }
84
+
85
+ /* Modal Styles */
86
+ .tipjar-modal {
87
+ z-index: 9999;
88
+ }
89
+
90
+ .tipjar-modal-backdrop {
91
+ animation: tipjar-fade-in 0.2s ease-out;
92
+ }
93
+
94
+ .tipjar-modal-content {
95
+ animation: tipjar-slide-up 0.3s ease-out;
96
+ }
97
+
98
+ @keyframes tipjar-fade-in {
99
+ from {
100
+ opacity: 0;
101
+ }
102
+ to {
103
+ opacity: 1;
104
+ }
105
+ }
106
+
107
+ @keyframes tipjar-slide-up {
108
+ from {
109
+ opacity: 0;
110
+ transform: translateY(1rem);
111
+ }
112
+ to {
113
+ opacity: 1;
114
+ transform: translateY(0);
115
+ }
116
+ }
117
+
118
+ /* Amount Button Styles */
119
+ .tipjar-amount-button {
120
+ border: 2px solid #e5e7eb;
121
+ background-color: white;
122
+ color: #374151;
123
+ font-weight: 500;
124
+ }
125
+
126
+ .tipjar-amount-button:hover {
127
+ border-color: #3b82f6;
128
+ background-color: #eff6ff;
129
+ }
130
+
131
+ .tipjar-amount-button.tipjar-amount-active {
132
+ border-color: #3b82f6;
133
+ background-color: #3b82f6;
134
+ color: white;
135
+ }
136
+
137
+ /* Dark Theme Support */
138
+ @media (prefers-color-scheme: dark) {
139
+ .tipjar-modal-content {
140
+ background-color: #1f2937;
141
+ color: #f3f4f6;
142
+ }
143
+
144
+ .tipjar-modal-header h3 {
145
+ color: #f3f4f6;
146
+ }
147
+
148
+ .tipjar-modal-header p {
149
+ color: #9ca3af;
150
+ }
151
+
152
+ .tipjar-amount-button {
153
+ background-color: #374151;
154
+ border-color: #4b5563;
155
+ color: #f3f4f6;
156
+ }
157
+
158
+ .tipjar-amount-button:hover {
159
+ background-color: #4b5563;
160
+ border-color: #60a5fa;
161
+ }
162
+
163
+ .tipjar-modal-close {
164
+ color: #9ca3af;
165
+ }
166
+
167
+ .tipjar-modal-close:hover {
168
+ color: #f3f4f6;
169
+ }
170
+ }
171
+
172
+ /* Responsive Design */
173
+ @media (max-width: 640px) {
174
+ .tipjar-button {
175
+ padding: 0.625rem 0.875rem;
176
+ font-size: 0.75rem;
177
+ }
178
+
179
+ .tipjar-button-icon {
180
+ width: 1.25rem;
181
+ height: 1.25rem;
182
+ }
183
+
184
+ .tipjar-modal-content {
185
+ margin: 1rem;
186
+ max-width: calc(100% - 2rem);
187
+ }
188
+
189
+ .tipjar-modal-amounts .grid {
190
+ grid-template-columns: 1fr;
191
+ }
192
+ }
193
+
194
+ /* Position Classes */
195
+ .fixed {
196
+ position: fixed;
197
+ }
198
+
199
+ .bottom-4 {
200
+ bottom: 1rem;
201
+ }
202
+
203
+ .right-4 {
204
+ right: 1rem;
205
+ }
206
+
207
+ .left-4 {
208
+ left: 1rem;
209
+ }
210
+
211
+ .top-4 {
212
+ top: 1rem;
213
+ }
@@ -0,0 +1,93 @@
1
+ module TipjarHelper
2
+ def tipjar_button(options = {})
3
+ config = RailsTipjar.config
4
+
5
+ position = options[:position] || config.position
6
+ custom_class = options[:class] || config.button_class
7
+ z_index = options[:z_index] || config.z_index
8
+
9
+ # Simple direct link mode when payment_link is set
10
+ if config.payment_link && !config.use_modal
11
+ link_to config.payment_link,
12
+ target: "_blank",
13
+ rel: "noopener",
14
+ class: "tipjar-button #{config_position_classes(position)} #{custom_class}",
15
+ style: "z-index: #{z_index};" do
16
+ content_tag(:span, class: "tipjar-button-icon") do
17
+ raw config.icon_svg
18
+ end +
19
+ content_tag(:span, config.button_text, class: "tipjar-button-text")
20
+ end
21
+ else
22
+ # Original modal mode
23
+ content_tag :div,
24
+ class: "tipjar-container",
25
+ data: {
26
+ controller: "tipjar",
27
+ tipjar_payment_links_value: config.payment_links.to_json,
28
+ tipjar_theme_value: config.theme,
29
+ tipjar_modal_title_value: config.modal_title,
30
+ tipjar_modal_description_value: config.modal_description,
31
+ tipjar_custom_amounts_value: config.custom_amounts.to_json,
32
+ tipjar_analytics_enabled_value: config.analytics_enabled
33
+ } do
34
+ render partial: "tipjar/button", locals: {
35
+ position: position,
36
+ custom_class: custom_class,
37
+ z_index: z_index,
38
+ config: config
39
+ }
40
+ end
41
+ end
42
+ end
43
+
44
+ def tipjar_link(text = nil, options = {})
45
+ config = RailsTipjar.config
46
+ text ||= config.button_text
47
+ amount = options[:amount] || :medium
48
+
49
+ link_url = if amount.is_a?(Symbol)
50
+ config.payment_links[amount]
51
+ else
52
+ config.payment_links[:custom] || config.payment_links[:medium]
53
+ end
54
+
55
+ link_options = {
56
+ target: "_blank",
57
+ rel: "noopener",
58
+ class: "tipjar-link #{options[:class]}",
59
+ data: {
60
+ tipjar_amount: amount,
61
+ tipjar_analytics: config.analytics_enabled
62
+ }
63
+ }
64
+
65
+ link_to text, link_url, link_options.merge(options.except(:amount, :class))
66
+ end
67
+
68
+ def tipjar_modal(options = {})
69
+ config = RailsTipjar.config
70
+
71
+ render partial: "tipjar/modal", locals: {
72
+ config: config,
73
+ options: options
74
+ }
75
+ end
76
+
77
+ private
78
+
79
+ def config_position_classes(position)
80
+ case position
81
+ when :bottom_right
82
+ "fixed bottom-4 right-4"
83
+ when :bottom_left
84
+ "fixed bottom-4 left-4"
85
+ when :top_right
86
+ "fixed top-4 right-4"
87
+ when :top_left
88
+ "fixed top-4 left-4"
89
+ else
90
+ "fixed bottom-4 right-4"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,16 @@
1
+ <button
2
+ data-tipjar-target="button"
3
+ data-action="click->tipjar#openModal"
4
+ class="tipjar-button <%= config_position_classes(position) %> <%= custom_class %>"
5
+ style="z-index: <%= z_index %>;"
6
+ aria-label="<%= config.button_text %>"
7
+ >
8
+ <span class="tipjar-button-icon">
9
+ <%= raw config.icon_svg %>
10
+ </span>
11
+ <span class="tipjar-button-text">
12
+ <%= config.button_text %>
13
+ </span>
14
+ </button>
15
+
16
+ <%= render "tipjar/modal", config: config %>
@@ -0,0 +1,74 @@
1
+ <div
2
+ data-tipjar-target="modal"
3
+ class="tipjar-modal hidden fixed inset-0 z-50 overflow-y-auto"
4
+ aria-labelledby="tipjar-modal-title"
5
+ role="dialog"
6
+ aria-modal="true"
7
+ >
8
+ <!-- Backdrop -->
9
+ <div
10
+ data-tipjar-target="backdrop"
11
+ class="tipjar-modal-backdrop fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
12
+ ></div>
13
+
14
+ <!-- Modal Content -->
15
+ <div class="flex items-center justify-center min-h-screen p-4">
16
+ <div class="tipjar-modal-content relative bg-white rounded-lg max-w-md w-full p-6 shadow-xl">
17
+ <!-- Close button -->
18
+ <button
19
+ type="button"
20
+ data-action="click->tipjar#closeModal"
21
+ class="tipjar-modal-close absolute top-4 right-4 text-gray-400 hover:text-gray-500"
22
+ aria-label="Close modal"
23
+ >
24
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
25
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
26
+ </svg>
27
+ </button>
28
+
29
+ <!-- Modal Header -->
30
+ <div class="tipjar-modal-header mb-4">
31
+ <h3 id="tipjar-modal-title" class="text-lg font-semibold text-gray-900">
32
+ <%= config.modal_title %>
33
+ </h3>
34
+ <p class="mt-2 text-sm text-gray-600">
35
+ <%= config.modal_description %>
36
+ </p>
37
+ </div>
38
+
39
+ <!-- Amount Selection -->
40
+ <div class="tipjar-modal-amounts mb-6">
41
+ <div class="grid grid-cols-2 gap-3">
42
+ <% config.custom_amounts.each do |amount_option| %>
43
+ <button
44
+ type="button"
45
+ data-tipjar-target="amountButton"
46
+ data-action="click->tipjar#selectAmount"
47
+ data-amount="<%= amount_option[:amount] %>"
48
+ data-default="<%= amount_option[:default] %>"
49
+ class="tipjar-amount-button px-4 py-3 border rounded-lg text-center transition-colors hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500 <%= 'tipjar-amount-active' if amount_option[:default] %>"
50
+ >
51
+ <%= amount_option[:label] %>
52
+ </button>
53
+ <% end %>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- Submit Button -->
58
+ <button
59
+ type="button"
60
+ data-action="click->tipjar#submitTip"
61
+ class="tipjar-submit-button w-full px-4 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
62
+ >
63
+ Continue to Payment
64
+ </button>
65
+
66
+ <!-- Powered by Stripe -->
67
+ <div class="tipjar-modal-footer mt-4 text-center">
68
+ <p class="text-xs text-gray-500">
69
+ Secure payment powered by Stripe
70
+ </p>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
@@ -0,0 +1 @@
1
+ pin "tipjar_controller", to: "tipjar_controller.js"
@@ -0,0 +1,92 @@
1
+ require "rails/generators/base"
2
+
3
+ module Tipjar
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates a RailsTipjar initializer and copies necessary files"
9
+
10
+ def copy_initializer
11
+ template "tipjar.rb", "config/initializers/tipjar.rb"
12
+ end
13
+
14
+ def add_stimulus_controller
15
+ if File.exist?("app/javascript/controllers/index.js")
16
+ append_to_file "app/javascript/controllers/index.js" do
17
+ <<~JS
18
+
19
+ // Register Tip Jar controller
20
+ import TipjarController from "tipjar_controller"
21
+ application.register("tipjar", TipjarController)
22
+ JS
23
+ end
24
+ else
25
+ say "Please manually register the tipjar Stimulus controller in your application", :yellow
26
+ end
27
+ end
28
+
29
+ def add_stylesheet
30
+ if File.exist?("app/assets/stylesheets/application.css")
31
+ append_to_file "app/assets/stylesheets/application.css" do
32
+ <<~CSS
33
+
34
+ /* Tip Jar Styles */
35
+ @import 'tipjar';
36
+ CSS
37
+ end
38
+ elsif File.exist?("app/assets/stylesheets/application.scss")
39
+ append_to_file "app/assets/stylesheets/application.scss" do
40
+ <<~SCSS
41
+
42
+ // Tip Jar Styles
43
+ @import 'tipjar';
44
+ SCSS
45
+ end
46
+ elsif File.exist?("app/assets/stylesheets/application.tailwind.css")
47
+ append_to_file "app/assets/stylesheets/application.tailwind.css" do
48
+ <<~CSS
49
+
50
+ /* Tip Jar Styles */
51
+ @import 'tipjar';
52
+ CSS
53
+ end
54
+ else
55
+ say "Please manually import 'tipjar' stylesheet in your application", :yellow
56
+ end
57
+ end
58
+
59
+ def add_helper_to_application_controller
60
+ inject_into_file "app/controllers/application_controller.rb",
61
+ after: "class ApplicationController < ActionController::Base\n" do
62
+ " helper TipjarHelper\n"
63
+ end
64
+ end
65
+
66
+ def display_post_install_message
67
+ say "\n", :green
68
+ say "RailsTipjar has been successfully installed!", :green
69
+ say "\n"
70
+ say "Next steps:", :yellow
71
+ say "1. Create Stripe Payment Links at https://dashboard.stripe.com/payment-links", :yellow
72
+ say "2. Configure your payment links in config/initializers/tipjar.rb", :yellow
73
+ say "3. Add <%= tipjar_button %> to your layout file", :yellow
74
+ say "\n"
75
+ say "Example configuration:", :cyan
76
+ say <<~CONFIG
77
+ RailsTipjar.configure do |config|
78
+ config.payment_links = {
79
+ small: "https://buy.stripe.com/your-link-1",
80
+ medium: "https://buy.stripe.com/your-link-2",
81
+ large: "https://buy.stripe.com/your-link-3"
82
+ }
83
+ config.position = :bottom_right
84
+ config.icon = :coffee
85
+ config.theme = :light
86
+ end
87
+ CONFIG
88
+ say "\n"
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ RailsTipjar.configure do |config|
4
+ # Configure your Stripe Payment Links
5
+ # Create payment links at: https://dashboard.stripe.com/payment-links
6
+ #
7
+ # You can create different links for different amounts, or use a single
8
+ # link that allows custom amounts
9
+ config.payment_links = {
10
+ small: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK",
11
+ medium: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK",
12
+ large: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK",
13
+ custom: "https://buy.stripe.com/test_REPLACE_WITH_YOUR_LINK"
14
+ }
15
+
16
+ # Position of the tip jar button
17
+ # Options: :bottom_right, :bottom_left, :top_right, :top_left
18
+ config.position = :bottom_right
19
+
20
+ # Icon to display in the button
21
+ # Options: :coffee, :heart, :star, :dollar, or custom SVG string
22
+ config.icon = :coffee
23
+
24
+ # Theme for the modal
25
+ # Options: :light, :dark, :auto
26
+ config.theme = :light
27
+
28
+ # Button text (shown on hover)
29
+ config.button_text = "Buy me a coffee"
30
+
31
+ # Modal customization
32
+ config.modal_title = "Support my work"
33
+ config.modal_description = "Your support helps me continue creating and maintaining this project."
34
+
35
+ # Custom tip amounts to display in the modal
36
+ config.custom_amounts = [
37
+ { amount: 5, label: "$5", default: false },
38
+ { amount: 10, label: "$10", default: true },
39
+ { amount: 25, label: "$25", default: false },
40
+ { amount: 50, label: "$50", default: false }
41
+ ]
42
+
43
+ # Additional CSS classes for the button
44
+ config.button_class = ""
45
+
46
+ # Z-index for the button and modal
47
+ config.z_index = 1000
48
+
49
+ # Enable analytics tracking
50
+ # Tracks: button clicks, modal opens, amount selections, and successful tips
51
+ # Works with Google Analytics 4 and Plausible out of the box
52
+ config.analytics_enabled = false
53
+ end
@@ -0,0 +1,88 @@
1
+ module RailsTipjar
2
+ class Configuration
3
+ attr_accessor :payment_links, :position, :icon, :theme, :button_text,
4
+ :modal_title, :modal_description, :custom_amounts,
5
+ :button_class, :z_index, :analytics_enabled, :payment_link,
6
+ :use_modal
7
+
8
+ def initialize
9
+ # Simple mode - just one link
10
+ @payment_link = nil
11
+ @use_modal = false # Default to direct link when payment_link is set
12
+
13
+ # Legacy modal mode
14
+ @payment_links = {}
15
+ @position = :bottom_right
16
+ @icon = :coffee
17
+ @theme = :light
18
+ @button_text = "Buy me a coffee"
19
+ @modal_title = "Support my work"
20
+ @modal_description = "Your support helps me continue creating and maintaining this project."
21
+ @custom_amounts = [
22
+ { amount: 5, label: "$5", default: false },
23
+ { amount: 10, label: "$10", default: true },
24
+ { amount: 25, label: "$25", default: false },
25
+ { amount: 50, label: "$50", default: false }
26
+ ]
27
+ @button_class = ""
28
+ @z_index = 1000
29
+ @analytics_enabled = false
30
+ end
31
+
32
+ def position_classes
33
+ case @position
34
+ when :bottom_right
35
+ "bottom-4 right-4"
36
+ when :bottom_left
37
+ "bottom-4 left-4"
38
+ when :top_right
39
+ "top-4 right-4"
40
+ when :top_left
41
+ "top-4 left-4"
42
+ else
43
+ "bottom-4 right-4"
44
+ end
45
+ end
46
+
47
+ def icon_svg
48
+ case @icon
49
+ when :coffee
50
+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
51
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25v7.5c0 2.485 2.099 4.5 4.688 4.5 1.935 0 3.597-1.126 4.313-2.733.715 1.607 2.377 2.733 4.313 2.733 2.588 0 4.687-2.015 4.687-4.5v-7.5z" />
52
+ </svg>'
53
+ when :heart
54
+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
55
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 8.5c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 4 3 6.015 3 8.5c0 7.5 9 11.5 9 11.5s9-4 9-11.5z" />
56
+ </svg>'
57
+ when :star
58
+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
59
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
60
+ </svg>'
61
+ when :dollar
62
+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
63
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
64
+ </svg>'
65
+ when :jar
66
+ '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
67
+ <!-- Jar lid -->
68
+ <rect x="8.5" y="3" width="7" height="1.5" rx="0.25" stroke-linecap="round" stroke-linejoin="round"/>
69
+ <!-- Jar threads/neck -->
70
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 4.5v1.5M15 4.5v1.5"/>
71
+ <!-- Jar body with taper -->
72
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 6c0 0 -1 0.5 -1 1.5v11.5a2 2 0 002 2h4a2 2 0 002-2V7.5c0-1 -1-1.5 -1-1.5"/>
73
+ <!-- Coins at bottom -->
74
+ <ellipse cx="10.5" cy="18.5" rx="1.5" ry="0.5" stroke-linecap="round"/>
75
+ <ellipse cx="14" cy="18" rx="1.5" ry="0.5" stroke-linecap="round"/>
76
+ <ellipse cx="12" cy="17.5" rx="1.5" ry="0.5" stroke-linecap="round"/>
77
+ <!-- Falling coin -->
78
+ <g class="tipjar-coin-animation">
79
+ <ellipse cx="12" cy="10" rx="1.5" ry="0.5" stroke-linecap="round"/>
80
+ <path stroke-linecap="round" d="M12 10v1.5"/>
81
+ </g>
82
+ </svg>'
83
+ else
84
+ @icon
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,26 @@
1
+ module RailsTipjar
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RailsTipjar
4
+
5
+ initializer "rails_tipjar.assets" do |app|
6
+ app.config.assets.paths << root.join("app/assets/javascripts")
7
+ app.config.assets.paths << root.join("app/assets/stylesheets")
8
+ app.config.assets.precompile += %w[
9
+ tipjar_controller.js
10
+ tipjar.css
11
+ ]
12
+ end
13
+
14
+ initializer "rails_tipjar.helpers" do
15
+ ActiveSupport.on_load(:action_controller_base) do
16
+ helper RailsTipjar::Engine.helpers
17
+ end
18
+ end
19
+
20
+ initializer "rails_tipjar.importmap", before: "importmap" do |app|
21
+ if defined?(Importmap)
22
+ app.config.importmap.paths << root.join("config/importmap.rb")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsTipjar
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rails_tipjar/version"
4
+ require_relative "rails_tipjar/configuration"
5
+ require_relative "rails_tipjar/engine" if defined?(Rails)
6
+
7
+ module RailsTipjar
8
+ class Error < StandardError; end
9
+
10
+ class << self
11
+ attr_accessor :configuration
12
+ end
13
+
14
+ def self.configure
15
+ self.configuration ||= Configuration.new
16
+ yield(configuration)
17
+ end
18
+
19
+ def self.config
20
+ self.configuration ||= Configuration.new
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module RailsTipjar
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_tipjar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Justin Paulson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: stimulus-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description: Easily add a customizable tip jar to any Rails application using Stripe
42
+ Payment Links
43
+ email:
44
+ - justinapaulson@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - Rakefile
51
+ - app/assets/javascripts/tipjar_controller.js
52
+ - app/assets/stylesheets/tipjar.css
53
+ - app/helpers/tipjar_helper.rb
54
+ - app/views/tipjar/_button.html.erb
55
+ - app/views/tipjar/_modal.html.erb
56
+ - config/importmap.rb
57
+ - lib/generators/tipjar/install/install_generator.rb
58
+ - lib/generators/tipjar/install/templates/tipjar.rb
59
+ - lib/rails_tipjar.rb
60
+ - lib/rails_tipjar/configuration.rb
61
+ - lib/rails_tipjar/engine.rb
62
+ - lib/rails_tipjar/version.rb
63
+ - sig/rails_tipjar.rbs
64
+ homepage: https://github.com/justinpaulson/rails_tipjar
65
+ licenses: []
66
+ metadata:
67
+ homepage_uri: https://github.com/justinpaulson/rails_tipjar
68
+ source_code_uri: https://github.com/justinpaulson/rails_tipjar
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 3.0.0
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.5.11
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: A reusable tip jar feature for Rails applications
88
+ test_files: []