@moneybar.online/moneybar 3.2.0 → 3.4.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.
package/README.md CHANGED
@@ -21,25 +21,35 @@ npm install @moneybar.online/moneybar
21
21
  import { MoneyBar } from '@moneybar.online/moneybar';
22
22
 
23
23
  const moneyBar = new MoneyBar({
24
+ actionFunction: 'generatePDF', // Function name that gets called
24
25
  appId: 'my-pdf-app',
25
- freeDownloadLimit: 3,
26
+ freeAttemptLimit: 3,
26
27
  supabase: {
27
28
  url: 'your-supabase-url',
28
29
  anonKey: 'your-anon-key'
29
30
  },
30
- payment: {
31
- productId: 'your-product-id'
31
+ payment: [{
32
+ provider: 'dodo',
33
+ productId: 'your-product-id',
34
+ mode: 'test'
35
+ }]
36
+ });
37
+
38
+ moneyBar.attachToButton('#action-btn', (userContext) => {
39
+ // Call the specified action function
40
+ if (typeof window[moneyBar.config.actionFunction] === 'function') {
41
+ window[moneyBar.config.actionFunction](userContext);
32
42
  }
33
43
  });
34
44
 
35
- moneyBar.attachToButton('#download-btn', (userContext) => {
45
+ // Define the function specified in actionFunction
46
+ function generatePDF(userContext) {
36
47
  if (userContext.isPremium) {
37
- generatePremiumPDF();
48
+ alert('Premium PDF generated with all features!');
38
49
  } else {
39
- generateBasicPDF();
40
- console.log(`${userContext.remaining} downloads remaining`);
50
+ alert(`Basic PDF generated! ${userContext.remaining} downloads remaining`);
41
51
  }
42
- });
52
+ }
43
53
  ```
44
54
 
45
55
  ### Option 2: CDN with Import Map (Recommended for HTML projects)
@@ -63,8 +73,9 @@ moneyBar.attachToButton('#download-btn', (userContext) => {
63
73
  import { MoneyBar } from 'https://cdn.jsdelivr.net/npm/@moneybar.online/moneybar/dist/index.browser.js';
64
74
 
65
75
  const moneyBar = new MoneyBar({
76
+ actionFunction: 'myAction', // Function name that gets called
66
77
  appId: 'my-app',
67
- freeDownloadLimit: 3,
78
+ freeAttemptLimit: 3,
68
79
  supabase: {
69
80
  url: 'your-supabase-url',
70
81
  anonKey: 'your-anon-key'
@@ -75,12 +86,20 @@ moneyBar.attachToButton('#download-btn', (userContext) => {
75
86
  });
76
87
 
77
88
  moneyBar.attachToButton('#action-btn', (userContext) => {
89
+ // Call the specified action function
90
+ if (typeof window[moneyBar.config.actionFunction] === 'function') {
91
+ window[moneyBar.config.actionFunction](userContext);
92
+ }
93
+ });
94
+
95
+ // Define the function specified in actionFunction
96
+ function myAction(userContext) {
78
97
  if (userContext.isPremium) {
79
98
  alert('Premium features unlocked!');
80
99
  } else {
81
100
  alert(`Free trial: ${userContext.remaining} uses left`);
82
101
  }
83
- });
102
+ }
84
103
  </script>
85
104
  </body>
86
105
  </html>
@@ -98,14 +117,30 @@ moneyBar.attachToButton('#download-btn', (userContext) => {
98
117
  import { MoneyBar } from 'https://cdn.jsdelivr.net/npm/@moneybar.online/moneybar/dist/index.bundle.js';
99
118
 
100
119
  const moneyBar = new MoneyBar({
120
+ actionFunction: 'myAction', // Function name that gets called
101
121
  appId: 'my-app',
102
- freeDownloadLimit: 3,
103
- // ... configuration
122
+ freeAttemptLimit: 3,
123
+ supabase: {
124
+ url: 'your-supabase-url',
125
+ anonKey: 'your-anon-key'
126
+ },
127
+ payment: {
128
+ productId: 'your-product-id'
129
+ }
104
130
  });
105
131
 
106
132
  moneyBar.attachToButton('#action-btn', (userContext) => {
107
- // Your logic here
133
+ // Call the specified action function
134
+ if (typeof window[moneyBar.config.actionFunction] === 'function') {
135
+ window[moneyBar.config.actionFunction](userContext);
136
+ }
108
137
  });
138
+
139
+ // Define the function specified in actionFunction
140
+ function myAction(userContext) {
141
+ // Your business logic here
142
+ alert(`Action executed! Premium: ${userContext.isPremium}, Remaining: ${userContext.remaining}`);
143
+ }
109
144
  </script>
110
145
  </body>
111
146
  </html>
@@ -134,81 +169,81 @@ moneyBar.attachToButton('#download-btn', (userContext) => {
134
169
  ### Quick Start Template
135
170
  Copy `moneybar-config-template.js` and `example-template.html` from this package for a ready-to-use template.
136
171
 
172
+ ## 🚀 Quick Start (2 minutes)
173
+
174
+ ### Files You Need:
175
+ 1. **`example-template.html`** - Working demo with all 3 use cases
176
+ 2. **`moneybar-config-template.js`** - Comprehensive configuration with 3 problem-focused examples
177
+
178
+ ### Try It Now:
179
+ ```bash
180
+ # After npm install
181
+ open example-template.html
182
+ ```
183
+
184
+ Then edit line 157 in `moneybar-config-template.js` to switch between use cases!
185
+
137
186
  ---
138
187
 
139
- ## 📱 Live Examples - Problem-Focused Demos
188
+ ## 📱 The 3 Money-Blocking Problems MoneyBar Solves
140
189
 
141
- ### Example #1: [`examples/value-first-demo.html`](examples/value-first-demo.html) + [`examples/value-first.config.js`](examples/value-first.config.js)
142
- **Problem #1: "Forced Sign-In Before Value"**
190
+ ### Problem #1: "Forced Sign-In Before Value"
191
+ **Revenue Impact: 40-60% boost**
143
192
  - ❌ **What Kills Revenue:** Users bounce before seeing your product works
144
193
  - ✅ **MoneyBar Solution:** Value-first trials with 3 free attempts
145
- - 💰 **Revenue Impact:** 40-60% more trial conversions
194
+ - 🎯 **Perfect for:** PDF generators, image tools, converters
146
195
  - 🎨 **Theme:** Emerald (trustworthy, fresh)
196
+ - **Config:** `VALUE_FIRST_CONFIG` in template
147
197
 
148
- ### Example #2: [`examples/feedback-intelligence.html`](examples/feedback-intelligence.html) + [`examples/feedback-intelligence.config.js`](examples/feedback-intelligence.config.js)
149
- **Problem #2: "Silent Drop-Off"**
198
+ ### Problem #2: "Silent Drop-Off"
199
+ **Revenue Impact: 20-30% boost**
150
200
  - ❌ **What Kills Revenue:** Users try but don't buy - you don't know why
151
201
  - ✅ **MoneyBar Solution:** Exit feedback capture at cancel moment
152
- - 💰 **Revenue Impact:** 20-30% better conversion through insights
202
+ - 🎯 **Perfect for:** SaaS trials, B2B tools, complex products
153
203
  - 🎨 **Theme:** Corporate (professional B2B)
204
+ - **Config:** `FEEDBACK_INTELLIGENCE_CONFIG` in template
154
205
 
155
- ### Example #3: [`examples/instant-payment-flow.html`](examples/instant-payment-flow.html) + [`examples/instant-payment.config.js`](examples/instant-payment.config.js)
156
- **Problem #3: "Broken Money Flow" + ⭐ Complete User Context Showcase**
206
+ ### Problem #3: "Broken Money Flow"
207
+ **Revenue Impact: 25-40% boost**
157
208
  - ❌ **What Kills Revenue:** Complex payments and broken sessions
158
- - ✅ **MoneyBar Solution:** Seamless Google Auth + instant payment + full user personalization
159
- - 💰 **Revenue Impact:** 25-40% higher payment completion
209
+ - ✅ **MoneyBar Solution:** Seamless Google Auth + instant payments
210
+ - 🎯 **Perfect for:** Templates, premium features, instant upgrades
160
211
  - 🎨 **Theme:** Dark (modern, premium)
161
- - **⭐ Features ALL user context properties with live demo**
162
-
163
- ---
164
-
165
- ## 🚨 The 3 Money-Blocking Stages for Indie Hackers
166
-
167
- ### Problem #1: "Forced Sign-In Before Value"
168
- **❌ What Kills Revenue:** Users bounce before seeing your product works
169
- - No evidence = no trust = no money
170
- - Users don't want to create "yet another account"
171
- - They leave before seeing what they would have paid for
172
-
173
- **✅ MoneyBar Solution:** Value-first trials with limited attempts
174
- - Give 2-3 free attempts - no sign-in required
175
- - Users experience value immediately
176
- - Only ask for payment when they're convinced
212
+ - **Config:** `INSTANT_PAYMENT_CONFIG` in template
177
213
 
178
- **📄 Example:** [`value-first-demo.html`](examples/value-first-demo.html)
179
- **💰 Revenue Impact:** 40-60% more trial conversions
214
+ ## 🔧 How to Use
180
215
 
181
- ---
182
-
183
- ### Problem #2: "Silent Drop-Off"
184
- **❌ What Kills Revenue:** Users try your tool but don't buy - you don't know why
185
- - User experiences tool → it works → but still doesn't pay
186
- - You have no data on why they left
187
- - You keep guessing instead of learning
188
-
189
- **✅ MoneyBar Solution:** Exit feedback capture at cancel moment
190
- - 1-click micro survey when users cancel upgrade
191
- - Ask "What stopped you from buying?" with preset options
192
- - Convert failure into product intelligence
216
+ ### Step 1: Choose Your Use Case
217
+ Edit line 157 in `moneybar-config-template.js`:
193
218
 
194
- **📄 Example:** [`feedback-intelligence.html`](examples/feedback-intelligence.html)
195
- **💰 Revenue Impact:** 20-30% better conversion through insights
219
+ ```javascript
220
+ // Pick one:
221
+ window.APP_CONFIG = VALUE_FIRST_CONFIG; // PDF/Image/Converter tools
222
+ window.APP_CONFIG = FEEDBACK_INTELLIGENCE_CONFIG; // SaaS/B2B products
223
+ window.APP_CONFIG = INSTANT_PAYMENT_CONFIG; // Templates/Premium features
224
+ ```
196
225
 
197
- ---
226
+ ### Step 2: Update Credentials (REQUIRED!)
227
+ ⚠️ **Before testing, you MUST replace these placeholder values or you'll get errors:**
198
228
 
199
- ### Problem #3: "Broken Money Flow"
200
- **❌ What Kills Revenue:** Complex payments and broken sessions
201
- - Payment flow is too long (every extra click = revenue death)
202
- - No Google/Apple Pay integration
203
- - After paying, user doesn't return cleanly to the tool
229
+ ```javascript
230
+ supabase: {
231
+ url: 'https://your-project.supabase.co', // Replace with your Supabase project URL
232
+ anonKey: 'your-anon-key' // Replace with your Supabase anon key
233
+ },
234
+ payment: [{
235
+ provider: 'dodo', // ✅ Payment provider
236
+ productId: 'your-product-id', // ✅ Replace with your DodoPayments product ID
237
+ mode: 'test' // ✅ Set to 'test' or 'live'
238
+ }]
239
+ ```
204
240
 
205
- **✅ MoneyBar Solution:** Seamless Google Auth + instant payment
206
- - One-click Google sign-in
207
- - Instant payment integration
208
- - User returns directly to unlocked tool
241
+ **🚨 Common Error:** If you get "Cannot read properties of null (reading 'AuthClient')" error:
242
+ - You haven't updated the Supabase credentials above
243
+ - Replace ALL placeholder values with your actual credentials
209
244
 
210
- **📄 Example:** [`instant-payment-flow.html`](examples/instant-payment-flow.html)
211
- **💰 Revenue Impact:** 25-40% higher payment completion
245
+ ### Step 3: Test the Demo
246
+ Open `example-template.html` in your browser and try the button!
212
247
 
213
248
  ---
214
249
 
@@ -237,22 +272,6 @@ function myAction(userContext) {
237
272
  }
238
273
  ```
239
274
 
240
- ### Multiple Buttons
241
- ```javascript
242
- moneyBar.attachToButtons({
243
- buttons: [
244
- {
245
- selector: '#free-btn',
246
- downloadCallback: (ctx) => downloadFreeTemplate(ctx)
247
- },
248
- {
249
- selector: '#premium-btn',
250
- downloadCallback: (ctx) => downloadPremiumTemplate(ctx),
251
- isPremium: true
252
- }
253
- ]
254
- });
255
- ```
256
275
 
257
276
  ### Feedback Collection Configuration
258
277
  ```javascript
@@ -266,10 +285,19 @@ const moneyBar = new MoneyBar({
266
285
  option2: 'Need more features',
267
286
  option3: 'Just testing'
268
287
  },
269
- email: {
270
- resendApiKey: 'your-resend-key',
271
- fromEmail: 'feedback@yourapp.com'
272
- }
288
+ email: [
289
+ {
290
+ provider: 'resend',
291
+ apiKey: 'your-resend-api-key',
292
+ fromEmail: 'feedback@yourapp.com'
293
+ }
294
+ // Future providers can be added here:
295
+ // {
296
+ // provider: 'sendgrid',
297
+ // apiKey: 'your-sendgrid-api-key',
298
+ // fromEmail: 'feedback@yourapp.com'
299
+ // }
300
+ ]
273
301
  }
274
302
  });
275
303
  ```
@@ -306,7 +334,7 @@ const moneyBar = new MoneyBar({
306
334
  const moneyBar = new MoneyBar({
307
335
  // Core required config
308
336
  appId: 'my-app', // Unique identifier for your app
309
- freeDownloadLimit: 3, // Free attempts before paywall
337
+ freeAttemptLimit: 3, // Free attempts before paywall
310
338
 
311
339
  // Supabase configuration (required)
312
340
  supabase: {
@@ -315,10 +343,11 @@ const moneyBar = new MoneyBar({
315
343
  },
316
344
 
317
345
  // Payment configuration (required)
318
- payment: {
346
+ payment: [{
347
+ provider: 'dodo', // Payment provider
319
348
  productId: 'prod_xyz', // DodoPayments product ID
320
349
  mode: 'test' // 'test' or 'live'
321
- }
350
+ }]
322
351
  });
323
352
  ```
324
353
 
@@ -388,10 +417,11 @@ function myAction(userContext) {
388
417
  MoneyBar supports DodoPayments for seamless checkout:
389
418
 
390
419
  ```javascript
391
- payment: {
420
+ payment: [{
421
+ provider: 'dodo',
392
422
  productId: 'your-dodo-product-id',
393
423
  mode: 'test' // or 'live'
394
- }
424
+ }]
395
425
  ```
396
426
 
397
427
  ---
@@ -22,13 +22,13 @@ import { createClient } from '@supabase/supabase-js';
22
22
  *
23
23
  * const moneyBar = new MoneyBar({
24
24
  * appId: 'my-pdf-app',
25
- * freeDownloadLimit: 3,
25
+ * freeAttemptLimit: 3,
26
26
  * supabase: { url: 'https://xyz.supabase.co', anonKey: 'anon_key' },
27
27
  * payment: { productId: 'prod_xyz' }
28
28
  * });
29
29
  *
30
30
  * // Single button - handles all monetization automatically
31
- * moneyBar.attachToButton('#download-btn', (userContext) => {
31
+ * moneyBar.attachToButton('#action-btn', (userContext) => {
32
32
  * // Your action logic here - with user context for personalization
33
33
  * if (userContext.isPremium) {
34
34
  * generatePremiumPDF();
@@ -214,10 +214,10 @@ class MoneyBar {
214
214
  }
215
215
  const status = {
216
216
  currentCount: count,
217
- limit: this.config.freeDownloadLimit,
217
+ limit: this.config.freeAttemptLimit,
218
218
  isPremium,
219
219
  isAuthenticated: !!this.currentUser,
220
- remaining: Math.max(0, this.config.freeDownloadLimit - count)
220
+ remaining: Math.max(0, this.config.freeAttemptLimit - count)
221
221
  };
222
222
  // Cache the status
223
223
  this.cachedStatus = status;
@@ -229,10 +229,10 @@ class MoneyBar {
229
229
  // Return safe defaults on error
230
230
  const errorStatus = {
231
231
  currentCount: 0,
232
- limit: this.config.freeDownloadLimit,
232
+ limit: this.config.freeAttemptLimit,
233
233
  isPremium: false,
234
234
  isAuthenticated: false,
235
- remaining: this.config.freeDownloadLimit
235
+ remaining: this.config.freeAttemptLimit
236
236
  };
237
237
  // Cache error status for short time to avoid repeated failures
238
238
  this.cachedStatus = errorStatus;
@@ -297,24 +297,35 @@ class MoneyBar {
297
297
  if (!this.config.payment) {
298
298
  throw new Error('Payment configuration not provided');
299
299
  }
300
+ const payment = this.config.payment?.find(p => p.provider === 'dodo');
301
+ if (!payment) {
302
+ throw new Error('No dodo payment provider configured');
303
+ }
300
304
  const requestBody = {
301
305
  email: this.currentUser.email,
302
- product_id: this.config.payment.productId,
303
- mode: this.config.payment.mode || 'test',
306
+ product_id: payment.productId,
307
+ mode: payment.mode || 'test',
304
308
  app_id: this.config.appId,
305
309
  return_url: `${window.location.origin}${window.location.pathname}?payment=success`
306
310
  };
307
- console.log(`🔥 [SUPABASE API] createPayment called for email: ${this.currentUser.email} | Time: ${new Date().toISOString()}`);
311
+ console.log(`🔥 [PAYMENT API] createPayment called for provider: ${payment.provider}, email: ${this.currentUser.email} | Time: ${new Date().toISOString()}`);
308
312
  try {
309
- const response = await fetch(`${this.config.supabase.url}/functions/v1/create-payment`, {
310
- method: 'POST',
311
- headers: {
312
- 'Content-Type': 'application/json',
313
- 'Authorization': `Bearer ${this.config.supabase.anonKey}`
314
- },
315
- body: JSON.stringify(requestBody)
316
- });
317
- console.log(`🔥 [SUPABASE API] createPayment response: ${response.status}`);
313
+ // Route to appropriate payment API based on provider
314
+ let response;
315
+ if (payment.provider === 'dodo') {
316
+ response = await fetch(`${this.config.supabase.url}/functions/v1/create-payment`, {
317
+ method: 'POST',
318
+ headers: {
319
+ 'Content-Type': 'application/json',
320
+ 'Authorization': `Bearer ${this.config.supabase.anonKey}`
321
+ },
322
+ body: JSON.stringify(requestBody)
323
+ });
324
+ }
325
+ else {
326
+ throw new Error(`Payment provider '${payment.provider}' is not yet supported`);
327
+ }
328
+ console.log(`🔥 [PAYMENT API] createPayment response for ${payment.provider}: ${response.status}`);
318
329
  if (this.config.options?.debug) {
319
330
  //console.log('🔍 DEBUG: createPayment response status:', response.status);
320
331
  }
@@ -517,8 +528,8 @@ class MoneyBar {
517
528
  // Only increment count after successful execution - with limit check
518
529
  const newCount = await this.incrementDownloadCount();
519
530
  // Double-check: if server returned count exceeding limit, something went wrong
520
- if (newCount > this.config.freeDownloadLimit) {
521
- console.error(`⚠️ Server returned count (${newCount}) exceeding limit (${this.config.freeDownloadLimit})`);
531
+ if (newCount > this.config.freeAttemptLimit) {
532
+ console.error(`⚠️ Server returned count (${newCount}) exceeding limit (${this.config.freeAttemptLimit})`);
522
533
  // Don't update UI with invalid state
523
534
  return;
524
535
  }
@@ -2318,7 +2329,26 @@ class MoneyBar {
2318
2329
  }
2319
2330
  }
2320
2331
  async sendFeedbackEmail(feedbackData) {
2321
- const emailConfig = this.config.feedback?.email || this.config.email;
2332
+ // Get email config - handle both new array format and legacy format
2333
+ let emailConfig;
2334
+ if (this.config.feedback?.email) {
2335
+ // New array format - use first provider (typically resend)
2336
+ if (Array.isArray(this.config.feedback.email)) {
2337
+ emailConfig = this.config.feedback.email[0];
2338
+ }
2339
+ else {
2340
+ // Legacy format fallback
2341
+ emailConfig = this.config.feedback.email;
2342
+ }
2343
+ }
2344
+ else if (this.config.email) {
2345
+ // Deprecated legacy email config
2346
+ emailConfig = {
2347
+ provider: 'resend',
2348
+ apiKey: this.config.email.resendApiKey,
2349
+ fromEmail: this.config.email.fromEmail
2350
+ };
2351
+ }
2322
2352
  if (!emailConfig) {
2323
2353
  throw new Error('Email configuration not available');
2324
2354
  }
@@ -2331,19 +2361,26 @@ User Email: ${feedbackData.email || 'Not provided'}
2331
2361
  Timestamp: ${feedbackData.timestamp}
2332
2362
  User Agent: ${feedbackData.userAgent}
2333
2363
  `.trim();
2334
- const response = await fetch('https://api.resend.com/emails', {
2335
- method: 'POST',
2336
- headers: {
2337
- 'Content-Type': 'application/json',
2338
- 'Authorization': `Bearer ${emailConfig.resendApiKey}`
2339
- },
2340
- body: JSON.stringify({
2341
- from: emailConfig.fromEmail,
2342
- to: [emailConfig.fromEmail], // Send feedback to yourself
2343
- subject: `Feedback from ${this.config.appId}`,
2344
- text: emailBody
2345
- })
2346
- });
2364
+ // Handle different email providers
2365
+ let response;
2366
+ if (emailConfig.provider === 'resend' || !emailConfig.provider) {
2367
+ response = await fetch('https://api.resend.com/emails', {
2368
+ method: 'POST',
2369
+ headers: {
2370
+ 'Content-Type': 'application/json',
2371
+ 'Authorization': `Bearer ${emailConfig.apiKey || emailConfig.resendApiKey}`
2372
+ },
2373
+ body: JSON.stringify({
2374
+ from: emailConfig.fromEmail,
2375
+ to: [emailConfig.fromEmail], // Send feedback to yourself
2376
+ subject: `Feedback from ${this.config.appId}`,
2377
+ text: emailBody
2378
+ })
2379
+ });
2380
+ }
2381
+ else {
2382
+ throw new Error(`Unsupported email provider: ${emailConfig.provider}`);
2383
+ }
2347
2384
  if (!response.ok) {
2348
2385
  throw new Error(`Email API error: ${response.status}`);
2349
2386
  }
@@ -2765,8 +2802,8 @@ User Agent: ${feedbackData.userAgent}
2765
2802
  throw new Error('supabase.url is required');
2766
2803
  if (!this.config.supabase?.anonKey)
2767
2804
  throw new Error('supabase.anonKey is required');
2768
- if (typeof this.config.freeDownloadLimit !== 'number' || this.config.freeDownloadLimit < 0) {
2769
- throw new Error('freeDownloadLimit must be a non-negative number');
2805
+ if (typeof this.config.freeAttemptLimit !== 'number' || this.config.freeAttemptLimit < 0) {
2806
+ throw new Error('freeAttemptLimit must be a non-negative number');
2770
2807
  }
2771
2808
  }
2772
2809
  async initializeSupabase() {
@@ -2786,7 +2823,15 @@ User Agent: ${feedbackData.userAgent}
2786
2823
  return;
2787
2824
  }
2788
2825
  // Validate product ID format
2789
- const productId = this.config.payment.productId;
2826
+ const payment = this.config.payment?.find(p => p.provider === 'dodo');
2827
+ if (!payment) {
2828
+ this.paymentConnectionFailed = true;
2829
+ if (this.config.options?.debug) {
2830
+ console.warn('No dodo payment provider configured');
2831
+ }
2832
+ return;
2833
+ }
2834
+ const productId = payment.productId;
2790
2835
  if (!productId || !productId.startsWith('pdt_') || productId.length < 10) {
2791
2836
  this.paymentConnectionFailed = true;
2792
2837
  if (this.config.options?.debug) {