77 Commits
Author SHA1 Message Date
Jarek Radosz cf83c0693c DEV: Deprecate the plugin (#281) 2025-07-14 15:12:17 +01:00
dependabot[bot] c7d7e14217 Bump rack from 3.1.12 to 3.1.14 (#275)
Bumps [rack](https://github.com/rack/rack) from 3.1.12 to 3.1.14.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v3.1.12...v3.1.14)

---
updated-dependencies:
- dependency-name: rack
  dependency-version: 3.1.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 12:50:34 -06:00
Mark Doerr 9e5f61832d FEATURE: Add support for SEK currency (#276)
Ref: https://meta.discourse.org/t/adding-new-currency-options-for-the-subscriptions-plugin/316003/12
2025-07-07 08:42:18 -06:00
dependabot[bot] 46f9fea9e2 Bump rack from 3.1.12 to 3.1.16 (#278)
Bumps [rack](https://github.com/rack/rack) from 3.1.12 to 3.1.16.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v3.1.12...v3.1.16)

---
updated-dependencies:
- dependency-name: rack
  dependency-version: 3.1.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 17:57:28 +02:00
Jarek Radosz fe53f2cbc0 DEV: Clean up helpers (#279)
* remove an unused helper
* move a single-use helper to its call-site as a getter
* convert `format-currency` to a pure function helper
2025-06-20 21:36:54 +02:00
Jarek Radosz 7107966af6 DEV: Update linting config and run gjs-codemod (#277) 2025-06-05 11:40:13 +01:00
Derek Rushforth 33d033c23b FIX: Campaign banner size when sidebar is active (#274) 2025-04-14 10:47:35 -07:00
Derek Rushforth 4771d81bfa UX: Campaign banner layout improvements (#273) 2025-04-10 13:12:38 -07:00
Blake Erickson 100b276c9d DEV: Add pagination to plans request (#272)
When pulling up a users subscriptions a 500 might occur if we don't find
their plan. If your account happens to have over 100 plans this can be
the cause of the 500 error. This change adds pagination when fetching
plans form stripe, so that we fetch them all.

https://meta.discourse.org/t/problem-with-500-error-with-subscriptions/360808?u=blake
2025-04-10 07:35:31 -06:00
Loïc Guitaut 3dd59a60da DEV: Add system spec to check core features are working fine (#271) 2025-04-09 14:56:06 +02:00
Derek Rushforth b0af86f12b Campaign banner styles (#270) 2025-03-28 14:21:54 -07:00
Jarek Radosz 854b3f223f DEV: Update linting (#269) 2025-03-17 11:41:43 +00:00
Penar Musaraj 9e38a69dbb fix spec for full page logins (#268) 2025-03-13 13:20:21 -04:00
Tomas Vavrda 208ecb66a3 FEATURE: Add support for CZK currency (#265) 2025-02-18 14:48:04 -07:00
Tomas Vavrda 832c52d5d7 FIX: FontAwesome 6 compatibility icon upgrade (#264)
Mitigates `Deprecation notice: The icon name "donate" has been updated to "circle-dollar-to-slot". Please use the new name in your code. Old names will be removed in Q2 2025. [deprecation id: discourse.fontawesome-6-upgrade] [info: https://meta.discourse.org/t/325349]` message.
2025-02-15 20:51:08 +01:00
dependabot[bot] cc43d32af7 Bump rack from 3.0.11 to 3.0.12 (#263)
Bumps [rack](https://github.com/rack/rack) from 3.0.11 to 3.0.12.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/commits/v3.0.12)

---
updated-dependencies:
- dependency-name: rack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 23:27:47 +00:00
David Taylor da48acad9a DEV: Bump dependencies and fix linting (#262) 2025-02-06 17:57:53 +01:00
David Taylor 721c4f28f1 DEV: Pin version for Discourse <3.5.0.beta1-dev (#261) 2025-02-05 20:17:39 +01:00
Discourse Translator Bot b4f7be4274 Update translations (#260) 2025-01-21 15:56:02 +01:00
David Taylor 001a25c63c DEV: Colocate component templates (#259) 2025-01-09 17:43:06 +01:00
Discourse Translator Bot a780c77f6c Update translations (#257) 2024-12-18 15:19:57 +01:00
Blake Erickson 02ba5e1ff2 FEATURE: Add support for PLN currency (#258)
Stripe supports the Polish złoty so we can add support for it.

https://meta.discourse.org/t/342040
2024-12-17 14:58:05 -07:00
Discourse Translator Bot 2c46e3cffb Update translations (#256) 2024-12-10 20:17:27 +01:00
Kelv 73db0b208a DEV: Update more deprecated Font Awesome icon names (#253) 2024-12-06 20:40:32 +01:00
Discourse Translator Bot cdd797c68f Update translations (#255) 2024-12-06 18:05:25 +01:00
Blake Erickson 1811b5955d FEATURE: Add support for CHF currency (#254)
Stripe supports the Swiss Franc so we can add support for it.

https://meta.discourse.org/t/316003
2024-12-05 11:01:31 -07:00
Blake Erickson d82b34080b DEV: Fix typo in site setting description (#252)
change permantly to permanently
2024-12-04 11:02:16 -07:00
Kelv 9263d97d64 DEV: Update deprecated Font Awesome icon names (#251) 2024-12-04 02:36:19 +01:00
Jarek Radosz 692f410ab5 DEV: Update components tests (#250) 2024-11-29 19:51:21 +01:00
David Taylor 3149828c22 DEV: Convert to native class syntax (#249) 2024-11-29 16:42:50 +01:00
David Taylor c7a35e1c89 DEV: Update linting (#248) 2024-11-28 17:14:17 +01:00
David Taylor 12cac3827c DEV: Update linting (#247) 2024-11-20 18:34:34 +01:00
dependabot[bot] 9a0369f272 Bump cross-spawn from 7.0.3 to 7.0.6 (#246)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 10:57:13 +00:00
David Taylor 7b15f57af3 DEV: Update eslint config (#245) 2024-11-19 11:31:14 +01:00
dependabot[bot] ce2f0899c6 Bump rexml from 3.3.6 to 3.3.9 (#244)
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.6 to 3.3.9.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.6...v3.3.9)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 20:40:26 +01:00
Gabriel Grubba dff47e2a3a DEV: Update plugin annotations (#243) 2024-10-25 09:48:56 +01:00
Blake Erickson f40d6f9cad DEV: Add some more verbose logging for debugging groups (#242)
Sometimes users aren't added to groups so adding some more logging
statements to aid in debugging.
2024-10-18 14:29:42 -06:00
Kelv 185071aa03 DEV: Switch to use pnpm (#241) 2024-10-14 14:39:36 +02:00
Bianca Nenciu f47400a9b1 DEV: Fix mismatched column types (#240)
The primary key is usually a bigint column, but the foreign key columns
are usually of integer type. This can lead to issues when joining these
columns due to mismatched types and different value ranges.
2024-10-10 16:29:35 +03:00
Blake EricksonandJarek Radosz eecb17698f FIX: Redirect to the pricing table page when enabled (#239)
If the pricing table is enabled the `/s` route should redirect to the
pricing table route and vice versa.

Co-authored-by: Jarek Radosz <[email protected]>
2024-10-02 17:53:20 -06:00
Jarek Radosz 937d099692 DEV: Prefix all test names (#238) 2024-09-18 01:49:28 +02:00
dependabot[bot] ef820ca2e0 Bump micromatch from 4.0.5 to 4.0.8 (#237)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-01 12:32:55 +02:00
dependabot[bot] f1b9c967c3 Bump rexml from 3.3.3 to 3.3.6 (#229)
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.3 to 3.3.6.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-29 15:02:37 -06:00
Discourse Translator Bot dd846ae87d Update translations (#235) 2024-08-29 15:01:43 -06:00
Blake Erickson 9c5ec86de7 DEV: Guest payments follow up (#236)
Just some minor follow up changes to PR #234
2024-08-29 14:49:03 -06:00
Blake Erickson fa5e95860b DEV: Fetch related guest payments (#234) 2024-08-28 21:51:34 -06:00
Blake Erickson 700c5e7727 FIX: Payments not showing up for users (#233)
Pricing table payments for one off purchases need to be attached to the
customer or they don't show up on the user's billing payments page.
2024-08-28 15:03:22 -06:00
Blake Erickson a2c70deeff FIX: Create customer for one off pricing table purchases (#232)
When using the Stripe Pricing Table for one-off purchases
(non-subscription) stripe doesn't automatically create a customer
record. This was causing the pricing table webhook
endpoint to end prematurely and not add users to their appropriate
group after purchase. Now if we detect that there is no customer record
we will make an api call to Stripe to create one.
2024-08-28 11:27:55 -06:00
Blake Erickson 71643a023d DEV: Use warn instead of info for Rails logger (#231)
Follow up to: e92dd39928
2024-08-28 09:17:08 -06:00
Blake Erickson e92dd39928 DEV: Add verbose logging site setting (#230)
To aid in debugging I'm adding a verbose logging site setting. I'm
adding this because I'm currently getting some errors in production that
I'm unable to replicate locally.

See: https://meta.discourse.org/t/323450/4
2024-08-27 18:40:12 -06:00
Blake Erickson f0b4984cee FIX: Pricing table for one-off purchases (#228)
When using the Stripe Pricing table for one-off purchases
(non-subscriptions) the webhook was encountering an error because it was
expecting subscription information which does not exist for one-off
purchases. This fix addresses that issue ensuring that no errors occur,
that users are added to the appropriate group, and that uses can see
their purchase on the billing page.
2024-08-20 13:32:32 -06:00
Discourse Translator Bot 52cae7b1a9 Update translations (#227) 2024-08-13 16:32:20 +02:00
dependabot[bot] 0f52101f2e Bump rexml from 3.2.8 to 3.3.3 (#226)
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.8 to 3.3.3.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.8...v3.3.3)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 13:20:22 +02:00
Natalie Tay 2824f955c2 DEV: Pin plugin for Discourse < 3.4.0.beta1-dev (#225) 2024-08-02 17:12:58 +08:00
Discourse Translator Bot bb96d77056 Update translations (#224) 2024-07-31 00:15:07 +02:00
Blake Erickson 41c443ab23 FIX: Decimal point truncation (#223)
Amounts like $45.80 were being displayed as $45.8. Which doesn't look
correct for currency.

Bug Report: https://meta.discourse.org/t/316007
2024-07-19 07:18:28 -06:00
Discourse Translator Bot 403c966514 Update translations (#221) 2024-07-16 22:14:17 +02:00
Blake Erickson 572aa0f4a9 FIX: Exception with campaign refresh data job (#222)
Depending on the invoices in stripe there could be some nil values for
things so we need to account for those in this job or an exception will
be thrown causing the job to fail.
2024-07-16 11:54:56 -06:00
Blake Erickson c02193943b FIX: Admins unable to cancel a subscription for a user (#220) 2024-07-12 14:33:04 -06:00
Discourse Translator Bot 0fca89a1f7 Update translations (#219) 2024-07-09 23:12:43 +02:00
Blake Erickson 3cc113973f FEATURE: Site Setting to enable automatic tax (#218)
FEATURE: Site Setting to enable automatic tax

There is now a new site setting called
`discourse_subscriptions_enable_automatic_tax` that can be enabled if
you would like to automatically compute tax rates on all new
subscriptions.
2024-07-08 13:22:02 -06:00
dependabot[bot] a3b4521a76 Bump braces from 3.0.2 to 3.0.3 (#217)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-16 22:31:59 +02:00
Discourse Translator Bot e9817406f1 Update translations (#216) 2024-05-31 12:30:13 +02:00
Loïc Guitaut 381e866009 DEV: Update rubocop-discourse to version 3.8.0 (#215) 2024-05-28 11:52:36 +02:00
Ted Johansson 804f5303f4 FIX: Add missing transaction error translation (#214) 2024-05-24 18:27:00 +08:00
dependabot[bot] cac4bdd4ff Bump rexml from 3.2.6 to 3.2.8 (#213)
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.6 to 3.2.8.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.6...v3.2.8)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-17 00:02:28 +02:00
Blake Erickson 542af4cf16 FIX: Campaign banner should link to pricing table when enabled (#212)
If the new pricing table is enabled the campaign banner should link to
the pricing table route.
2024-05-15 11:44:27 -06:00
Discourse Translator Bot 512f37a1d3 Update translations (#211) 2024-05-14 16:20:09 +02:00
Blake Erickson d63c84eca5 FIX: One-time purchase pending invoice item (#210)
This change ensures we attach the invoice item to the invoice to avoid
any occurrences of an empty invoice being paid with pending invoice
items.
2024-05-07 08:57:37 -06:00
Blake Erickson e132913db4 FIX: Login required for Stripe Checkout (#209)
If an anonymous user tries to subscribe we need to show them a log in
message first. We currently don't have support for anonymous
subscriptions.
2024-05-03 17:01:51 -06:00
Blake Erickson 5eba613f8f FIX: Prevent duplicate stripe subscription list requests (#208)
Users can have multiple customer records in our db, so we need to filter
out non-unique stripe customer ids before fetching subscriptions for each.
2024-05-03 12:38:26 -06:00
Blake Erickson 66e8857c20 DEV: Stop deleting customers on cancel (#207)
Instead of deleting customers on cancel we will now update the
subscription status to canceled. This way we can have some visibility on
which users have canceled.
2024-05-02 13:38:30 -06:00
Discourse Translator Bot aaa4baec8a Update translations (#205) 2024-04-30 21:57:46 +02:00
Blake Erickson b1920caf7d FIX: Auto-Create Support Campaign button (#206)
The Auto-Create Support Campaign button was just showing an infinite "is
loading" spinner so you couldn't even click on it.

This fix allows the button to work again and only show the loading
spinner when it is actually loading.

See: https://meta.discourse.org/t/discourse-subscriptions/140818/653?u=blake
2024-04-30 09:27:04 -06:00
Blake Erickson 7ee0b696ba FIX: Update cc save button missing (#204) 2024-04-30 06:00:55 -06:00
Blake Erickson 6f54494a3b FIX: Allow active status for updated webhook (#203)
Likely there was an API change at some point now that we are using the
latest version of the stripe API. When the
`customer.subscription.updated` webhook is called we should also accept
the status of "active".
2024-04-29 15:50:05 -06:00
Blake Ericksonandspirobel 45754baa00 FEATURE: Add new stripe based pricing table (#202)
This commit adds an optional new stripe based pricing table.
If the user is logged in, the email field will be prepopulated with the users email.

The pricing table can be configured in the stripe dashboard.

Once the discourse_subscriptions_pricing_table setting is filled with the pricing table embed code from the stripe dashboard,
the pricing table will be displayed on /s/subscriptions

For more details see https://stripe.com/docs/payments/checkout/pricing-table
---------

Co-authored-by: spirobel <[email protected]>
2024-04-29 12:47:58 -06:00
200 changed files with 9210 additions and 6539 deletions
+4
View File
@@ -1,3 +1,7 @@
< 3.5.0.beta5-dev: 7309c937dddb1933ac66aa2d3e13d071060d30ff
< 3.5.0.beta1-dev: b4f7be4274ada9400238f8692e705669034c1709
< 3.4.0.beta2-dev: 692f410ab59e329d0aaf537cd772698f83c32a8a
< 3.4.0.beta1-dev: bb96d77056a9ef842e01e46fd980320d427073ad
< 3.3.0.beta1-dev: 0dcf17de59bdf82c0233bb630dad0ce407d1ae76
3.1.999: c4f8220a5572ef9a42a9a71500e001d0df206c43
2.9.0.beta9: 39b7253c082270e33839f4b0821fbf75abfdd826
-1
View File
@@ -1 +0,0 @@
module.exports = require("@discourse/lint-configs/eslint");
+2
View File
@@ -0,0 +1,2 @@
engine-strict = true
auto-install-peers = false
+68 -44
View File
@@ -7,71 +7,95 @@ GIT
GEM
remote: https://rubygems.org/
specs:
activesupport (7.1.3.2)
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
ast (2.4.2)
base64 (0.2.0)
bigdecimal (3.1.6)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
drb (2.2.1)
i18n (1.14.3)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
ast (2.4.3)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
drb (2.2.3)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
racc (~> 1.7)
json (2.7.1)
language_server-protocol (3.17.0.3)
minitest (5.22.2)
mutex_m (0.2.0)
parallel (1.24.0)
parser (3.3.0.5)
json (2.12.2)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
minitest (5.25.5)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
prettier_print (1.2.1)
racc (1.7.3)
prism (1.4.0)
racc (1.8.1)
rack (3.1.16)
rainbow (3.1.1)
regexp_parser (2.9.0)
rexml (3.2.6)
rubocop (1.62.0)
regexp_parser (2.10.0)
rubocop (1.75.8)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.1)
parser (>= 3.3.0.4)
rubocop-capybara (2.20.0)
rubocop (~> 1.41)
rubocop-discourse (3.7.1)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-discourse (3.12.1)
activesupport (>= 6.1)
rubocop (>= 1.59.0)
rubocop-capybara (>= 2.0.0)
rubocop-factory_bot (>= 2.0.0)
rubocop-rspec (>= 2.25.0)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41)
rubocop-rspec (2.27.1)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
lint_roller (>= 1.1.0)
rubocop (>= 1.73.2)
rubocop-capybara (>= 2.22.0)
rubocop-factory_bot (>= 2.27.0)
rubocop-rails (>= 2.30.3)
rubocop-rspec (>= 3.0.1)
rubocop-rspec_rails (>= 2.31.0)
rubocop-factory_bot (2.27.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.6.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
syntax_tree (6.2.0)
prettier_print (>= 1.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
PLATFORMS
ruby
@@ -82,4 +106,4 @@ DEPENDENCIES
translations-manager!
BUNDLED WITH
2.5.4
2.6.9
+3
View File
@@ -1,5 +1,8 @@
# Discourse Subscriptions Plugin
> [!IMPORTANT]
> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574
Discourse Subscriptions allows site owners to sell recurring and one-time purchase subscriptions that grant access to a group on a Discourse instance.
For more information, please see: https://meta.discourse.org/t/discourse-subscriptions-plugin/140818
@@ -53,7 +53,7 @@ module DiscourseSubscriptions
params.require(:id)
begin
refund_subscription(params[:id]) if params[:refund]
subscription = ::Stripe::Subscription.delete(params[:id])
subscription = ::Stripe::Subscription.cancel(params[:id])
customer =
Customer.find_by(
@@ -61,11 +61,8 @@ module DiscourseSubscriptions
customer_id: subscription[:customer],
)
Subscription.delete_by(external_id: params[:id])
if customer
user = ::User.find(customer.user_id)
customer.delete
group = plan_group(subscription[:plan])
group.remove(user) if group
end
@@ -83,6 +80,7 @@ module DiscourseSubscriptions
expand: ["data.plan.product"],
limit: PAGE_LIMIT,
starting_after: start,
status: "all",
)
end
@@ -9,6 +9,7 @@ module DiscourseSubscriptions
layout false
before_action :set_api_key
skip_before_action :check_xhr
skip_before_action :redirect_to_login_if_required
skip_before_action :verify_authenticity_token, only: [:create]
@@ -27,48 +28,121 @@ module DiscourseSubscriptions
end
case event[:type]
when "checkout.session.completed"
checkout_session = event[:data][:object]
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("checkout.session.completed data: #{checkout_session}")
end
email = checkout_session[:customer_email]
return head 200 if checkout_session[:status] != "complete"
return render_json_error "email not found" if !email
if checkout_session[:customer].nil?
customer = ::Stripe::Customer.create({ email: email })
customer_id = customer[:id]
else
customer_id = checkout_session[:customer]
end
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Looking up user with email: #{email}")
end
user = ::User.find_by_username_or_email(email)
return render_json_error "user not found" if !user
discourse_customer = Customer.create(user_id: user.id, customer_id: customer_id)
subscription = checkout_session[:subscription]
if subscription.present?
Subscription.create(customer_id: discourse_customer.id, external_id: subscription)
end
line_items =
::Stripe::Checkout::Session.list_line_items(checkout_session[:id], { limit: 1 })
item = line_items[:data].first
group = plan_group(item[:price])
group.add(user) unless group.nil?
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Line item with group name meta data: #{item[:price]}")
if group.nil?
Rails.logger.warn("Group not found or not listed in metadata!")
else
Rails.logger.warn("Group: #{group.name}")
end
end
discourse_customer.product_id = item[:price][:product]
discourse_customer.save!
if !subscription.nil?
::Stripe::Subscription.update(
subscription,
{ metadata: { user_id: user.id, username: user.username } },
)
end
when "customer.subscription.created"
when "customer.subscription.updated"
customer =
Customer.find_by(
customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product],
)
subscription = event[:data][:object]
status = subscription[:status]
return head 200 if !%w[complete active].include?(status)
customer = find_active_customer(subscription[:customer], subscription[:plan][:product])
return render_json_error "customer not found" if !customer
return head 200 if event[:data][:object][:status] != "complete"
update_status(customer.id, subscription[:id], status)
user = ::User.find_by(id: customer.user_id)
return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan])
if group = plan_group(subscription[:plan])
group.add(user)
end
when "customer.subscription.deleted"
customer =
Customer.find_by(
customer_id: event[:data][:object][:customer],
product_id: event[:data][:object][:plan][:product],
)
subscription = event[:data][:object]
customer = find_active_customer(subscription[:customer], subscription[:plan][:product])
return render_json_error "customer not found" if !customer
Subscription.find_by(
customer_id: customer.id,
external_id: event[:data][:object][:id],
)&.destroy!
update_status(customer.id, subscription[:id], subscription[:status])
user = ::User.find(customer.user_id)
return render_json_error "user not found" if !user
if group = plan_group(event[:data][:object][:plan])
if group = plan_group(subscription[:plan])
group.remove(user)
end
customer.destroy!
end
head 200
end
private
def update_status(customer_id, subscription_id, status)
discourse_subscription =
Subscription.find_by(customer_id: customer_id, external_id: subscription_id)
discourse_subscription.update(status: status) if discourse_subscription
end
def find_active_customer(customer_id, product_id)
Customer
.joins(:subscriptions)
.where(customer_id: customer_id, product_id: product_id)
.where(
Subscription.arel_table[:status].eq(nil).or(
Subscription.arel_table[:status].not_eq("canceled"),
),
)
.first
end
end
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
module DiscourseSubscriptions
class PricingtableController < ::ApplicationController
requires_plugin DiscourseSubscriptions::PLUGIN_NAME
def index
head 200
end
end
end
@@ -11,7 +11,20 @@ module DiscourseSubscriptions
requires_login except: %i[index contributors show]
def index
head 200
begin
product_ids = Product.all.pluck(:external_id)
products = []
if product_ids.present? && is_stripe_configured?
response = ::Stripe::Product.list({ ids: product_ids, active: true })
products = response[:data].map { |p| serialize_product(p) }
end
render_json_dump products
rescue ::Stripe::InvalidRequestError => e
render_json_error e.message
end
end
def contributors
@@ -72,14 +85,19 @@ module DiscourseSubscriptions
promo_code_id = promo_code[:id] if promo_code
transaction =
::Stripe::Subscription.create(
customer: customer[:id],
items: [{ price: params[:plan] }],
metadata: metadata_user,
trial_period_days: trial_days,
promotion_code: promo_code_id,
)
subscription_params = {
customer: customer[:id],
items: [{ price: params[:plan] }],
metadata: metadata_user,
trial_period_days: trial_days,
promotion_code: promo_code_id,
}
if SiteSetting.discourse_subscriptions_enable_automatic_tax
subscription_params[:automatic_tax] = { enabled: true }
end
transaction = ::Stripe::Subscription.create(subscription_params)
payment_intent = retrieve_payment_intent(transaction[:latest_invoice]) if transaction[
:status
@@ -87,16 +105,28 @@ module DiscourseSubscriptions
else
coupon_id = promo_code[:coupon][:id] if promo_code && promo_code[:coupon] &&
promo_code[:coupon][:id]
invoice_params = { customer: customer[:id] }
if SiteSetting.discourse_subscriptions_enable_automatic_tax
invoice_params[:automatic_tax] = { enabled: true }
end
invoice = ::Stripe::Invoice.create(invoice_params)
invoice_item =
::Stripe::InvoiceItem.create(
customer: customer[:id],
price: params[:plan],
discounts: [{ coupon: coupon_id }],
invoice: invoice[:id],
)
invoice = ::Stripe::Invoice.create(customer: customer[:id])
transaction = ::Stripe::Invoice.finalize_invoice(invoice[:id])
payment_intent = retrieve_payment_intent(transaction[:id]) if transaction[:status] ==
"open"
if payment_intent.nil?
return(
render_json_error I18n.t("js.discourse_subscriptions.subscribe.transaction_error")
)
end
transaction = ::Stripe::Invoice.pay(invoice[:id]) if payment_intent[:status] ==
"successful"
end
@@ -137,7 +167,11 @@ module DiscourseSubscriptions
)
if transaction[:object] == "subscription"
Subscription.create(customer_id: customer.id, external_id: transaction[:id])
Subscription.create(
customer_id: customer.id,
external_id: transaction[:id],
status: transaction[:status],
)
end
end
@@ -156,7 +190,17 @@ module DiscourseSubscriptions
def current_user_products
return [] if current_user.nil?
Customer.select(:product_id).where(user_id: current_user.id).map { |c| c.product_id }.compact
Customer
.joins(:subscriptions)
.where(user_id: current_user.id)
.where(
Subscription.arel_table[:status].eq(nil).or(
Subscription.arel_table[:status].not_eq("canceled"),
),
)
.select(:product_id)
.distinct
.pluck(:product_id)
end
def serialize_plans(plans)
@@ -27,10 +27,31 @@ module DiscourseSubscriptions
payments = ::Stripe::PaymentIntent.list(customer: customer_id)
payments_from_invoices =
payments[:data].select { |payment| invoice_ids.include?(payment[:invoice]) }
data = data | payments_from_invoices
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Payments from invoices: #{payments_from_invoices}")
end
# Pricing table one-off purchases do not have invoices
payments_without_invoices =
payments[:data].select { |payment| payment[:invoice].nil? }
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Payments without invoices: #{payments_without_invoices}")
end
data = data | payments_from_invoices | payments_without_invoices
end
end
if SiteSetting.discourse_subscriptions_pricing_table_enabled && current_user.email
related_guest_payments = fetch_guest_payments(current_user.email)
if SiteSetting.discourse_subscriptions_enable_verbose_logging
Rails.logger.warn("Related guest payments: #{related_guest_payments}")
end
data = data | related_guest_payments
end
data = data.sort_by { |pmt| pmt[:created] }.reverse
render_json_dump data
@@ -42,12 +63,13 @@ module DiscourseSubscriptions
private
def parse_invoices(all_invoices, product_ids)
invoices_with_products =
all_invoices[:data].select do |invoice|
invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data]
all_invoices[:data].select do |invoice|
invoice_lines = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data]
if invoice_lines
invoice_product_id = parse_invoice_lines(invoice_lines)
product_ids.include?(invoice_product_id)
end
end
end
def parse_invoice_lines(invoice_lines)
@@ -57,6 +79,40 @@ module DiscourseSubscriptions
invoice_lines[:plan][:product]
invoice_product_id
end
def fetch_guest_payments(email)
guest_payments = []
starting_after = nil
begin
loop do
# Fetch charges in batches of 100, using pagination with starting_after
charges =
::Stripe::Charge.list(
limit: 100,
starting_after: starting_after,
expand: ["data.payment_intent"],
)
charges[:data].each do |charge|
# Check if the charge is associated with the given email and has no customer ID
if charge[:billing_details][:email] == email && charge[:customer].nil?
guest_payments << charge
end
end
# Check if there are more charges to fetch
break if charges[:data].count < 100
# Set starting_after to the last charge's ID for the next batch
starting_after = charges[:data].last[:id]
end
rescue ::Stripe::StripeError => e
render_json_error e.message
end
guest_payments
end
end
end
end
@@ -15,6 +15,7 @@ module DiscourseSubscriptions
begin
customer = Customer.where(user_id: current_user.id)
customer_ids = customer.map { |c| c.id } if customer
stripe_customer_ids = customer.map { |c| c.customer_id }.uniq if customer
subscription_ids =
Subscription.where("customer_id in (?)", customer_ids).pluck(
:external_id,
@@ -23,18 +24,25 @@ module DiscourseSubscriptions
subscriptions = []
if subscription_ids
plans = ::Stripe::Price.list(expand: ["data.product"], limit: 100)
prices = []
price_params = { limit: 100, expand: ["data.product"] }
loop do
response = ::Stripe::Price.list(price_params)
prices.concat(response[:data])
break unless response[:has_more]
price_params[:starting_after] = response[:data].last.id
end
all_subscriptions = []
customers =
::Stripe::Customer.list(email: current_user.email, expand: ["data.subscriptions"])
subscriptions =
customers[:data].map { |sub_customer| sub_customer[:subscriptions][:data] }.flatten(1)
subscriptions = subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
stripe_customer_ids.each do |stripe_customer_id|
customer_subscriptions =
::Stripe::Subscription.list(customer: stripe_customer_id, status: "all")
all_subscriptions.concat(customer_subscriptions[:data])
end
subscriptions = all_subscriptions.select { |sub| subscription_ids.include?(sub[:id]) }
subscriptions.map! do |subscription|
plan = plans[:data].find { |p| p[:id] == subscription[:items][:data][0][:price][:id] }
plan = prices.find { |p| p[:id] == subscription[:items][:data][0][:price][:id] }
subscription.to_h.except!(:plan)
subscription.to_h.merge(plan: plan, product: plan[:product].to_h.slice(:id, :name))
end
@@ -13,3 +13,20 @@ module DiscourseSubscriptions
end
end
end
# == Schema Information
#
# Table name: discourse_subscriptions_customers
#
# id :bigint not null, primary key
# customer_id :string not null
# product_id :string
# user_id :bigint
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_discourse_subscriptions_customers_on_customer_id (customer_id)
# index_discourse_subscriptions_customers_on_user_id (user_id)
#
@@ -4,3 +4,17 @@ module DiscourseSubscriptions
class Product < ActiveRecord::Base
end
end
# == Schema Information
#
# Table name: discourse_subscriptions_products
#
# id :bigint not null, primary key
# external_id :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_discourse_subscriptions_products_on_external_id (external_id)
#
@@ -5,3 +5,20 @@ module DiscourseSubscriptions
belongs_to :customer
end
end
# == Schema Information
#
# Table name: discourse_subscriptions_subscriptions
#
# id :bigint not null, primary key
# customer_id :bigint not null
# external_id :string not null
# created_at :datetime not null
# updated_at :datetime not null
# status :string
#
# Indexes
#
# index_discourse_subscriptions_subscriptions_on_customer_id (customer_id)
# index_discourse_subscriptions_subscriptions_on_external_id (external_id)
#
@@ -53,6 +53,14 @@ module DiscourseSubscriptions
"S$"
when "zar"
"R"
when "chf"
"CHF"
when "pln"
"zł"
when "czk"
"Kč"
when "sek"
"kr"
else
"$"
end
@@ -91,7 +91,7 @@ module DiscourseSubscriptions
params = {
full_name: I18n.t("js.discourse_subscriptions.campaign.supporters"),
title: I18n.t("js.discourse_subscriptions.campaign.supporter"),
flair_icon: "donate",
flair_icon: "circle-dollar-to-slot",
}
group.update(params)
@@ -154,26 +154,30 @@ module DiscourseSubscriptions
all_invoices =
::Stripe::Invoice.list(limit: 100, starting_after: current_set[:last_record])
current_set[:last_record] = all_invoices[:data].last[:id] if all_invoices[:data].present?
current_set[:has_more] = all_invoices[:has_more]
if all_invoices[:data].present?
current_set[:last_record] = all_invoices[:data].last[:id]
current_set[:has_more] = all_invoices[:has_more]
all_invoices[:data].each do |invoice|
customer_id = invoice[:customer]
next if invoice[:paid] != true
line_item = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] # Discourse only makes single-line item charges
# check if non-subscription and that the plan is active
if line_item[:plan] == nil && line_item[:price] &&
line_item[:price][:recurring] == nil && line_item[:price][:active] == true
product_id = line_item[:price][:product]
if product_ids.include? product_id
line_data = {
customer_id: customer_id,
product_id: product_id,
price: line_item[:price][:unit_amount],
}
one_time_payments << line_data
all_invoices[:data].each do |invoice|
customer_id = invoice[:customer]
next if invoice[:paid] != true
line_item = invoice[:lines][:data][0] if invoice[:lines] && invoice[:lines][:data] # Discourse only makes single-line item charges
# check if non-subscription and that the plan is active
if line_item && line_item[:plan] == nil && line_item[:price] &&
line_item[:price][:recurring] == nil && line_item[:price][:active] == true
product_id = line_item[:price][:product]
if product_ids.include? product_id
line_data = {
customer_id: customer_id,
product_id: product_id,
price: line_item[:price][:unit_amount],
}
one_time_payments << line_data
end
end
end
else
current_set[:has_more] = false
end
end
end
File diff suppressed because it is too large Load Diff
@@ -1,173 +0,0 @@
{{#if shouldShow}}
<div
class="campaign-banner"
style={{html-safe (concat "box-shadow: 5px 5px #" dropShadowColor)}}
>
<DButton @icon="times" @action={{this.dismissBanner}} class="close" />
<div class="campaign-banner-info" style={{html-safe this.bannerInfoStyle}}>
{{#if isGoalMet}}
<h2 class="campaign-banner-info-header">
{{i18n "discourse_subscriptions.campaign.success_title"}}
</h2>
<p class="campaign-banner-info-description">
{{i18n "discourse_subscriptions.campaign.success_body"}}
</p>
{{else}}
<h2 class="campaign-banner-info-header">
{{i18n "discourse_subscriptions.campaign.title"}}
</h2>
<p class="campaign-banner-info-description">
{{i18n "discourse_subscriptions.campaign.body"}}
</p>
{{#if product}}
<LinkTo
@route="subscribe.show"
@model={{product}}
@disabled={{product.subscribed}}
class="btn btn-primary campaign-banner-info-button"
>
{{d-icon "far-heart"}}
{{d-icon "heart" class="hover-heart"}}
{{i18n "discourse_subscriptions.campaign.button"}}
</LinkTo>
{{else}}
<LinkTo
@route="subscribe"
class="btn btn-primary campaign-banner-info-button"
>
{{d-icon "far-heart"}}
{{d-icon "heart" class="hover-heart"}}
{{i18n "discourse_subscriptions.campaign.button"}}
</LinkTo>
{{/if}}
{{/if}}
</div>
<div class="campaign-banner-progress">
{{#if isGoalMet}}
<div class="fireworks">
<div class="before"></div>
<div class="after"></div>
</div>
<div class="campaign-banner-progress-success"></div>
{{#if subscriberGoal}}
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=subscribers
goal=goalTarget
)
}}
{{i18n "discourse_subscriptions.campaign.subscribers"}}
</p>
{{else}}
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=(format-currency currency amountRaised)
goal=(format-currency currency goalTarget)
)
}}
{{i18n "discourse_subscriptions.campaign.raised"}}
</p>
{{#if showContributors}}
<ConditionalLoadingSpinner @condition={{loading}} @size="small">
<div class="campaign-banner-progress-users">
<p class="campaign-banner-progress-users-title">
<strong>
{{i18n
"discourse_subscriptions.campaign.recent_contributors"
}}
</strong>
</p>
<div class="campaign-banner-progress-users-avatars">
{{#each contributors as |contributor|}}
{{avatar
contributor
avatarTemplatePath="avatar_template"
usernamePath="username"
namePath="name"
imageSize="small"
}}
{{/each}}
</div>
</div>
</ConditionalLoadingSpinner>
{{/if}}
{{/if}}
{{else}}
{{#if subscriberGoal}}
<progress
class="campaign-banner-progress-bar"
value={{subscribers}}
max={{siteSettings.discourse_subscriptions_campaign_goal}}
></progress>
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=subscribers
goal=goalTarget
)
}}
{{i18n "discourse_subscriptions.campaign.subscribers"}}
</p>
{{else}}
<progress
class="campaign-banner-progress-bar"
value={{amountRaised}}
max={{siteSettings.discourse_subscriptions_campaign_goal}}
></progress>
<p class="campaign-banner-progress-description">
{{html-safe
(i18n
"discourse_subscriptions.campaign.goal_comparison"
current=(format-currency currency amountRaised)
goal=(format-currency currency goalTarget)
)
}}
{{i18n "discourse_subscriptions.campaign.raised"}}
</p>
{{/if}}
{{#if showContributors}}
<ConditionalLoadingSpinner @condition={{loading}} @size="small">
<div class="campaign-banner-progress-users">
<p class="campaign-banner-progress-users-title">
<strong>
{{i18n
"discourse_subscriptions.campaign.recent_contributors"
}}
</strong>
</p>
<div class="campaign-banner-progress-users-avatars">
{{#each contributors as |contributor|}}
{{avatar
contributor
avatarTemplatePath="avatar_template"
usernamePath="username"
namePath="name"
imageSize="small"
}}
{{/each}}
</div>
</div>
</ConditionalLoadingSpinner>
{{/if}}
{{/if}}
</div>
</div>
{{/if}}
@@ -1,187 +0,0 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import { later } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { setting } from "discourse/lib/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
const SIDEBAR_BODY_CLASS = "subscription-campaign-sidebar";
export default Component.extend({
router: service(),
dismissed: false,
loading: false,
dropShadowColor: setting(
"discourse_subscriptions_campaign_banner_shadow_color"
),
backgroundImageUrl: setting(
"discourse_subscriptions_campaign_banner_bg_image"
),
isSidebar: equal(
"siteSettings.discourse_subscriptions_campaign_banner_location",
"Sidebar"
),
subscribers: setting("discourse_subscriptions_campaign_subscribers"),
subscriberGoal: equal(
"siteSettings.discourse_subscriptions_campaign_type",
"Subscribers"
),
currency: setting("discourse_subscriptions_currency"),
amountRaised: setting("discourse_subscriptions_campaign_amount_raised"),
goalTarget: setting("discourse_subscriptions_campaign_goal"),
product: setting("discourse_subscriptions_campaign_product"),
showContributors: setting(
"discourse_subscriptions_campaign_show_contributors"
),
classNameBindings: ["isGoalMet:goal-met"],
init() {
this._super(...arguments);
this.set("contributors", []);
// add background-image url to stylesheet
if (this.backgroundImageUrl) {
const backgroundUrl = `url(${this.backgroundImageUrl}`.replace(/\\/g, "");
if (
document.documentElement.style.getPropertyValue(
"--campaign-background-image"
) !== backgroundUrl
) {
document.documentElement.style.setProperty(
"--campaign-background-image",
backgroundUrl
);
}
}
if (this.currentUser && this.showContributors) {
return ajax("/subscriptions/contributors", { method: "get" }).then(
(result) => {
this.setProperties({
contributors: result,
loading: false,
});
}
);
}
},
didInsertElement() {
this._super(...arguments);
if (this.isSidebar && this.shouldShow && !this.site.mobileView) {
document.body.classList.add(SIDEBAR_BODY_CLASS);
} else {
document.body.classList.remove(SIDEBAR_BODY_CLASS);
}
// makes sure to only play animation once, & not repeat on reload
if (this.isGoalMet) {
const successAnimationKey = this.keyValueStore.get(
"campaign_success_animation"
);
if (!successAnimationKey) {
later(() => {
this.keyValueStore.set({
key: "campaign_success_animation",
value: Date.now(),
});
document.body.classList.add("success-animation-off");
}, 7000);
} else {
document.body.classList.add("success-animation-off");
}
}
},
willDestroyElement() {
this._super(...arguments);
document.body.classList.remove(SIDEBAR_BODY_CLASS);
},
@discourseComputed("backgroundImageUrl")
bannerInfoStyle(backgroundImageUrl) {
if (!backgroundImageUrl) {
return "";
}
return `background-image: linear-gradient(
0deg,
rgba(var(--secondary-rgb), 0.75) 0%,
rgba(var(--secondary-rgb), 0.75) 100%),
var(--campaign-background-image);
background-size: cover;
background-repeat: no-repeat;`;
},
@discourseComputed(
"router.currentRouteName",
"currentUser",
"siteSettings.discourse_subscriptions_campaign_enabled",
"visible"
)
shouldShow(currentRoute, currentUser, enabled, visible) {
// do not show on admin or subscriptions pages
const showOnRoute =
currentRoute !== "discovery.s" &&
!currentRoute.split(".")[0].includes("admin") &&
currentRoute.split(".")[0] !== "subscribe";
if (!this.site.show_campaign_banner) {
return false;
}
// make sure not to render above main container when inside a topic
if (
this.connectorName === "above-main-container" &&
currentRoute.includes("topic")
) {
return false;
}
return showOnRoute && currentUser && enabled && visible;
},
@observes("dismissed")
_updateBodyClasses() {
if (this.dismissed) {
document.body.classList.remove(SIDEBAR_BODY_CLASS);
}
},
@discourseComputed("dismissed")
visible(dismissed) {
const dismissedBannerKey = this.keyValueStore.get(
"dismissed_campaign_banner"
);
const threeMonths = 2628000000 * 3;
const bannerDismissedTime = new Date(dismissedBannerKey);
const now = Date.now();
return (
(!dismissedBannerKey || now - bannerDismissedTime > threeMonths) &&
!dismissed
);
},
@discourseComputed
isGoalMet() {
const currentVolume = this.subscriberGoal
? this.subscribers
: this.amountRaised;
return currentVolume >= this.goalTarget;
},
@action
dismissBanner() {
this.set("dismissed", true);
this.keyValueStore.set({
key: "dismissed_campaign_banner",
value: Date.now(),
});
},
});
@@ -0,0 +1,92 @@
import Component, { Input } from "@ember/component";
import { fn } from "@ember/helper";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import ComboBox from "select-kit/components/combo-box";
export default class CreateCouponForm extends Component {
discountType = "amount";
discount = null;
promoCode = null;
active = false;
@discourseComputed
discountTypes() {
return [
{ id: "amount", name: "Amount" },
{ id: "percent", name: "Percent" },
];
}
@action
createNewCoupon() {
const createParams = {
promo: this.promoCode,
discount_type: this.discountType,
discount: this.discount,
active: this.active,
};
this.create(createParams);
}
@action
cancelCreate() {
this.cancel();
}
<template>
<div class="create-coupon-form">
<form class="form-horizontal">
<p>
<label for="promo_code">
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
</label>
<Input @type="text" name="promo_code" @value={{this.promoCode}} />
</p>
<p>
<label for="amount">
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
</label>
<ComboBox
@content={{this.discountTypes}}
@value={{this.discountType}}
@onChange={{fn (mut this.discountType)}}
/>
<Input
class="discount-amount"
@type="text"
name="amount"
@value={{this.discount}}
/>
</p>
<p>
<label for="active">
{{i18n "discourse_subscriptions.admin.coupons.active"}}
</label>
<Input @type="checkbox" name="active" @checked={{this.active}} />
</p>
</form>
<DButton
@action={{this.createNewCoupon}}
@label="discourse_subscriptions.admin.coupons.create"
@title="discourse_subscriptions.admin.coupons.create"
@icon="plus"
class="btn-primary btn btn-icon"
/>
<DButton
@action={{this.cancelCreate}}
label="cancel"
@title="cancel"
@icon="xmark"
class="btn btn-icon"
/>
</div>
</template>
}
@@ -1,50 +0,0 @@
<div class="create-coupon-form">
<form class="form-horizontal">
<p>
<label for="promo_code">
{{i18n "discourse_subscriptions.admin.coupons.promo_code"}}
</label>
<Input @type="text" name="promo_code" @value={{promoCode}} />
</p>
<p>
<label for="amount">
{{i18n "discourse_subscriptions.admin.coupons.discount"}}
</label>
<ComboBox
@content={{discountTypes}}
@value={{discountType}}
@onChange={{action (mut discountType)}}
/>
<Input
class="discount-amount"
@type="text"
name="amount"
@value={{discount}}
/>
</p>
<p>
<label for="active">
{{i18n "discourse_subscriptions.admin.coupons.active"}}
</label>
<Input @type="checkbox" name="active" @checked={{active}} />
</p>
</form>
<DButton
@action={{action "createNewCoupon"}}
@label="discourse_subscriptions.admin.coupons.create"
@title="discourse_subscriptions.admin.coupons.create"
@icon="plus"
class="btn-primary btn btn-icon"
/>
<DButton
@action={{action "cancelCreate"}}
label="cancel"
@title="cancel"
@icon="times"
class="btn btn-icon"
/>
</div>
@@ -1,32 +0,0 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
@discourseComputed
discountTypes() {
return [
{ id: "amount", name: "Amount" },
{ id: "percent", name: "Percent" },
];
},
discountType: "amount",
discount: null,
promoCode: null,
active: false,
actions: {
createNewCoupon() {
const createParams = {
promo: this.promoCode,
discount_type: this.discountType,
discount: this.discount,
active: this.active,
};
this.create(createParams);
},
cancelCreate() {
this.cancel();
},
},
});
@@ -0,0 +1,16 @@
import DButton from "discourse/components/d-button";
import routeAction from "discourse/helpers/route-action";
import { i18n } from "discourse-i18n";
const LoginRequired = <template>
<h3>{{i18n "discourse_subscriptions.subscribe.unauthenticated"}}</h3>
<DButton
@label="log_in"
@action={{routeAction "showLogin"}}
@icon="user"
class="btn btn-primary login-required subscriptions"
/>
</template>;
export default LoginRequired;
@@ -1,8 +0,0 @@
<h3>{{i18n "discourse_subscriptions.subscribe.unauthenticated"}}</h3>
<DButton
@label="log_in"
@action={{route-action "showLogin"}}
@icon="user"
class="btn btn-primary login-required subscriptions"
/>
@@ -4,7 +4,7 @@ import { Input } from "@ember/component";
import { fn, hash } from "@ember/helper";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import i18n from "discourse-common/helpers/i18n";
import { i18n } from "discourse-i18n";
export default class AdminCancelSubscription extends Component {
@tracked refund;
@@ -25,9 +25,13 @@ export default class AdminCancelSubscription extends Component {
@label="yes_value"
@action={{fn
@model.cancelSubscription
(hash [email protected] refund=this.refund)
(hash
[email protected]
refund=this.refund
closeModal=@closeModal
)
}}
@icon="times"
@icon="xmark"
@isLoading={{@model.subscription.loading}}
class="btn-danger"
/>
@@ -0,0 +1,42 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import PaymentPlan from "./payment-plan";
export default class PaymentOptions extends Component {
@discourseComputed("plans")
orderedPlans(plans) {
if (plans) {
return plans.sort((a, b) => (a.unit_amount > b.unit_amount ? 1 : -1));
}
}
didInsertElement() {
super.didInsertElement(...arguments);
if (this.plans && this.plans.length === 1) {
this.set("selectedPlan", this.plans[0].id);
}
}
@action
clickPlan(plan) {
this.set("selectedPlan", plan.id);
}
<template>
<p>
{{i18n "discourse_subscriptions.plans.select"}}
</p>
<div class="subscribe-buttons">
{{#each this.orderedPlans as |plan|}}
<PaymentPlan
@plan={{plan}}
@selectedPlan={{this.selectedPlan}}
@clickPlan={{this.clickPlan}}
/>
{{/each}}
</div>
</template>
}
@@ -1,13 +0,0 @@
<p>
{{i18n "discourse_subscriptions.plans.select"}}
</p>
<div class="subscribe-buttons">
{{#each orderedPlans as |plan|}}
<PaymentPlan
@plan={{plan}}
@selectedPlan={{selectedPlan}}
@clickPlan={{action "clickPlan"}}
/>
{{/each}}
</div>
@@ -1,23 +0,0 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
@discourseComputed("plans")
orderedPlans(plans) {
if (plans) {
return plans.sort((a, b) => (a.unit_amount > b.unit_amount ? 1 : -1));
}
},
didInsertElement() {
this._super(...arguments);
if (this.plans && this.plans.length === 1) {
this.set("selectedPlan", this.plans[0].id);
}
},
actions: {
clickPlan(plan) {
this.set("selectedPlan", plan.id);
},
},
});
@@ -0,0 +1,57 @@
import Component from "@ember/component";
import { concat } from "@ember/helper";
import { action } from "@ember/object";
import { tagName } from "@ember-decorators/component";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import formatCurrency from "../helpers/format-currency";
const RECURRING = "recurring";
@tagName("")
export default class PaymentPlan extends Component {
@discourseComputed("selectedPlan")
selectedClass(planId) {
return planId === this.plan.id ? "btn-primary" : "";
}
@discourseComputed("plan.type")
recurringPlan(type) {
return type === RECURRING;
}
@action
planClick() {
this.clickPlan(this.plan);
return false;
}
<template>
<DButton
@action={{this.planClick}}
class={{concatClass
"btn-discourse-subscriptions-subscribe"
this.selectedClass
}}
>
<span class="interval">
{{#if this.recurringPlan}}
{{i18n
(concat
"discourse_subscriptions.plans.interval.adverb."
this.plan.recurring.interval
)
}}
{{else}}
{{i18n "discourse_subscriptions.one_time_payment"}}
{{/if}}
</span>
<span class="amount">
{{formatCurrency this.plan.currency this.plan.amountDollars}}
</span>
</DButton>
</template>
}
@@ -1,21 +0,0 @@
<DButton
@action={{action "planClick"}}
class={{concat-class "btn-discourse-subscriptions-subscribe" selectedClass}}
>
<span class="interval">
{{#if this.recurringPlan}}
{{i18n
(concat
"discourse_subscriptions.plans.interval.adverb."
this.plan.recurring.interval
)
}}
{{else}}
{{i18n "discourse_subscriptions.one_time_payment"}}
{{/if}}
</span>
<span class="amount">
{{format-currency this.plan.currency this.plan.amountDollars}}
</span>
</DButton>

Some files were not shown because too many files have changed in this diff Show More