bullet_train 1.0.81 → 1.0.86
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/bullet-train.js +2 -0
- data/app/assets/javascripts/bullet-train.js.map +1 -0
- data/app/controllers/concerns/account/controllers/base.rb +9 -0
- data/app/helpers/base_helper.rb +5 -1
- data/app/models/concerns/memberships/base.rb +5 -0
- data/app/models/concerns/records/base.rb +8 -0
- data/app/models/concerns/teams/base.rb +19 -0
- data/app/views/layouts/docs.html.erb +4 -4
- data/config/locales/en/billing/products.en.yml +17 -0
- data/docs/billing/stripe.md +94 -38
- data/docs/index.md +1 -1
- data/docs/tunneling.md +4 -4
- data/lib/bullet_train/version.rb +1 -1
- data/lib/bullet_train.rb +6 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 849a48985baf2ad6e8e2fec56d4fabaad5bf1218012ee46c3af717b6c2328a61
|
4
|
+
data.tar.gz: ca355a5d67e5caf57159e4dcf9592b375bd492d96e53783c753a75d1230c0dc7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9be5e8da59d9d4a95a03d369500ebda69b40d2344f5a73015a77cd1fc29eeafc3a5c34f39086dc8af2b647093d17f9aea84b9f8c963b168b465d96ef1d17eab
|
7
|
+
data.tar.gz: e1d61f2ab4e6836e7fdb07c9fc79a3303a1a103ca8846cf630008718b6d7b7e1b93367e6a55f9949c2c9c731d0e69602c821f3449c1a35de3346bf4c3a3f996e
|
@@ -0,0 +1,2 @@
|
|
1
|
+
import{Controller as e}from"@hotwired/stimulus";function t(e){const t=(e.match(/^(?:\.\/)?(.+)(?:[_-]controller\..+?)$/)||[])[1];if(t)return t.replace(/_/g,"-").replace(/\//g,"--")}class i extends e{copy(){this.inputTarget.value=this.sourceTarget.innerText,this.inputTarget.select(),document.execCommand("copy"),this.buttonTarget.innerHTML='<i id="copied" class="fas fa-check w-4 h-4 block text-green-600"></i>',setTimeout(function(){document.getElementById("copied").innerHTML='<i class="far fa-copy w-4 h-4 block text-gray-600"></i>'},1500)}}i.targets=["source","input","button"];class r extends e{constructor(){super(...arguments),this.removeTrailingNewlines=e=>{e.element.innerHTML.match(/<br><\/div>$/)&&(e.element.innerHTML=e.element.innerHTML.slice(0,-10)+"</div>",this.removeTrailingNewlines(e))},this.removeTrailingWhitespace=e=>{e.element.innerHTML.match(/ <\/div>$/)?(e.element.innerHTML=e.element.innerHTML.slice(0,-12)+"</div>",this.removeTrailingWhitespace(e)):e.element.innerHTML.match(/ <\/div>$/)&&(e.element.innerHTML=e.element.innerHTML.slice(0,-13)+"</div>",this.removeTrailingWhitespace(e))}}resetOnSuccess(e){e.detail.success&&e.target.reset()}stripTrix(){this.trixFieldTargets.forEach(e=>{this.removeTrailingNewlines(e.editor),this.removeTrailingWhitespace(e.editor),e.parentElement.querySelector("input").value=e.innerHTML})}submitOnReturn(e){if((e.metaKey||e.ctrlKey)&&13==e.keyCode){e.preventDefault();let t=e.target.closest("form");this.submitForm(t)}}submitForm(e){e.requestSubmit?e.requestSubmit():e.querySelector("[type=submit]").click()}}r.targets=["trixField","scroll"];class s extends e{toggle(){const e=this.isWrapperHidden?this.showEventNameValue:this.hideEventNameValue;this.isWrapperHidden&&this.showWrapper(),this.wrapperTarget.dispatchEvent(new CustomEvent(e))}get isWrapperHidden(){return this.wrapperTarget.classList.contains(this.hiddenClass)}showWrapper(){this.wrapperTarget.classList.remove(this.hiddenClass)}hideWrapper(){this.wrapperTarget.classList.add(this.hiddenClass)}}s.targets=["wrapper"],s.classes=["hidden"],s.values={showEventName:String,hideEventName:String};const n=[[i,"clipboard_controller.js"],[r,"form_controller.js"],[s,"mobile_menu_controller.js"]].map(function(e){const i=e[0];return{identifier:t(e[1]),controllerConstructor:i}});document.addEventListener("turbo:load",()=>{navigator.userAgent.toLocaleLowerCase().includes("electron")&&document.body.classList.add("electron")});export{n as controllerDefinitions};
|
2
|
+
//# sourceMappingURL=bullet-train.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"bullet-train.js","sources":["../../../node_modules/@hotwired/stimulus-webpack-helpers/dist/stimulus-webpack-helpers.js","../../javascript/controllers/clipboard_controller.js","../../javascript/controllers/form_controller.js","../../javascript/controllers/mobile_menu_controller.js","../../javascript/controllers/index.js","../../javascript/electron/index.js"],"sourcesContent":["/*\nStimulus Webpack Helpers 1.0.0\nCopyright © 2021 Basecamp, LLC\n */\nfunction definitionsFromContext(context) {\n return context.keys()\n .map((key) => definitionForModuleWithContextAndKey(context, key))\n .filter((value) => value);\n}\nfunction definitionForModuleWithContextAndKey(context, key) {\n const identifier = identifierForContextKey(key);\n if (identifier) {\n return definitionForModuleAndIdentifier(context(key), identifier);\n }\n}\nfunction definitionForModuleAndIdentifier(module, identifier) {\n const controllerConstructor = module.default;\n if (typeof controllerConstructor == \"function\") {\n return { identifier, controllerConstructor };\n }\n}\nfunction identifierForContextKey(key) {\n const logicalName = (key.match(/^(?:\\.\\/)?(.+)(?:[_-]controller\\..+?)$/) || [])[1];\n if (logicalName) {\n return logicalName.replace(/_/g, \"-\").replace(/\\//g, \"--\");\n }\n}\n\nexport { definitionForModuleAndIdentifier, definitionForModuleWithContextAndKey, definitionsFromContext, identifierForContextKey };\n","import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = ['source', 'input', 'button']\n\n copy() {\n this.inputTarget.value = this.sourceTarget.innerText\n this.inputTarget.select()\n document.execCommand('copy')\n this.buttonTarget.innerHTML = '<i id=\"copied\" class=\"fas fa-check w-4 h-4 block text-green-600\"></i>'\n setTimeout(function () {\n document.getElementById('copied').innerHTML = '<i class=\"far fa-copy w-4 h-4 block text-gray-600\"></i>'\n }, 1500)\n }\n}\n","import { Controller } from \"@hotwired/stimulus\"\n\n// TODO Some of this feels really specific to the conversation messages form. Should we rename this controller?\nexport default class extends Controller {\n static targets = ['trixField', 'scroll']\n\n resetOnSuccess(e){\n if(e.detail.success) {\n e.target.reset();\n }\n }\n\n stripTrix(){\n this.trixFieldTargets.forEach(element => {\n this.removeTrailingNewlines(element.editor)\n this.removeTrailingWhitespace(element.editor)\n // When doing this as part of the form submission, Trix does not update the input element's value attribute fast enough.\n // In order to submit the stripped value, we manually update it here to fix the race condition\n element.parentElement.querySelector(\"input\").value = element.innerHTML\n })\n }\n\n submitOnReturn(e) {\n if((e.metaKey || e.ctrlKey) && e.keyCode == 13) {\n e.preventDefault();\n let form = e.target.closest(\"form\")\n this.submitForm(form)\n }\n }\n\n removeTrailingNewlines = (trixEditor) => {\n if (trixEditor.element.innerHTML.match(/<br><\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -10) + \"</div>\"\n this.removeTrailingNewlines(trixEditor)\n }\n }\n\n removeTrailingWhitespace = (trixEditor) => {\n if (trixEditor.element.innerHTML.match(/ <\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -12) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n } else if (trixEditor.element.innerHTML.match(/ <\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -13) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n }\n }\n\n submitForm(form) {\n // Right now, Safari and IE don't support the requestSubmit method which is required for Turbo\n // Doing form.submit() doesn't actually fire the submit event which Turbo needs\n if (form.requestSubmit) {\n form.requestSubmit()\n } else {\n form.querySelector(\"[type=submit]\").click()\n }\n }\n}\n","import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [ \"wrapper\"]\n static classes = [ \"hidden\" ] // necessary because stimulus-reveal will mess with the [hidden] attribute on the wrapper\n static values = {\n showEventName: String,\n hideEventName: String,\n }\n\n toggle() {\n const eventName = this.isWrapperHidden? this.showEventNameValue: this.hideEventNameValue\n if (this.isWrapperHidden) {\n this.showWrapper()\n }\n \n this.wrapperTarget.dispatchEvent(new CustomEvent(eventName))\n }\n \n get isWrapperHidden() {\n return this.wrapperTarget.classList.contains(this.hiddenClass)\n }\n \n showWrapper() {\n this.wrapperTarget.classList.remove(this.hiddenClass)\n }\n \n hideWrapper() {\n this.wrapperTarget.classList.add(this.hiddenClass)\n }\n}","import { identifierForContextKey } from \"@hotwired/stimulus-webpack-helpers\"\n\nimport ClipboardController from './clipboard_controller'\nimport FormController from './form_controller'\nimport MobileMenuController from './mobile_menu_controller'\n\nexport const controllerDefinitions = [\n [ClipboardController, 'clipboard_controller.js'],\n [FormController, 'form_controller.js'],\n [MobileMenuController, 'mobile_menu_controller.js'],\n].map(function(d) {\n const key = d[1]\n const controller = d[0]\n return {\n identifier: identifierForContextKey(key),\n controllerConstructor: controller\n }\n})\n","document.addEventListener(\"turbo:load\", () => {\n if (navigator.userAgent.toLocaleLowerCase().includes('electron')) {\n document.body.classList.add('electron')\n }\n})"],"names":["identifierForContextKey","key","logicalName","match","replace","Controller","copy","this","inputTarget","value","sourceTarget","innerText","select","document","execCommand","buttonTarget","innerHTML","setTimeout","getElementById","targets","removeTrailingNewlines","trixEditor","element","slice","removeTrailingWhitespace","resetOnSuccess","e","detail","success","target","reset","stripTrix","trixFieldTargets","forEach","editor","parentElement","querySelector","submitOnReturn","metaKey","ctrlKey","keyCode","preventDefault","form","closest","submitForm","requestSubmit","click","toggle","eventName","isWrapperHidden","showEventNameValue","hideEventNameValue","showWrapper","wrapperTarget","dispatchEvent","CustomEvent","classList","contains","hiddenClass","remove","hideWrapper","add","classes","values","showEventName","String","hideEventName","controllerDefinitions","ClipboardController","FormController","MobileMenuController","map","d","controller","identifier","controllerConstructor","addEventListener","navigator","userAgent","toLocaleLowerCase","includes","body"],"mappings":"gDAqBA,SAASA,EAAwBC,GAC7B,MAAMC,GAAeD,EAAIE,MAAM,2CAA6C,IAAI,GAChF,GAAID,EACA,OAAOA,EAAYE,QAAQ,KAAM,KAAKA,QAAQ,MAAO,sBCtBhCC,EAG3BC,OACEC,KAAKC,YAAYC,MAAQF,KAAKG,aAAaC,UAC3CJ,KAAKC,YAAYI,SACjBC,SAASC,YAAY,QACrBP,KAAKQ,aAAaC,UAAY,wEAC9BC,WAAW,WACTJ,SAASK,eAAe,UAAUF,UAAY,2DAC7C,SATEG,QAAU,CAAC,SAAU,QAAS,0BCAVd,yCA2B3Be,uBAA0BC,IACpBA,EAAWC,QAAQN,UAAUb,MAAM,kBACrCkB,EAAWC,QAAQN,UAAYK,EAAWC,QAAQN,UAAUO,MAAM,GAAI,IAAM,SAC5EhB,KAAKa,uBAAuBC,UAIhCG,yBAA4BH,IACtBA,EAAWC,QAAQN,UAAUb,MAAM,mBACrCkB,EAAWC,QAAQN,UAAYK,EAAWC,QAAQN,UAAUO,MAAM,GAAI,IAAM,SAC5EhB,KAAKiB,yBAAyBH,IACrBA,EAAWC,QAAQN,UAAUb,MAAM,qBAC5CkB,EAAWC,QAAQN,UAAYK,EAAWC,QAAQN,UAAUO,MAAM,GAAI,IAAM,SAC5EhB,KAAKiB,yBAAyBH,KArClCI,eAAeC,GACVA,EAAEC,OAAOC,SACVF,EAAEG,OAAOC,QAIbC,YACExB,KAAKyB,iBAAiBC,QAAQX,IAC5Bf,KAAKa,uBAAuBE,EAAQY,QACpC3B,KAAKiB,yBAAyBF,EAAQY,QAGtCZ,EAAQa,cAAcC,cAAc,SAAS3B,MAAQa,EAAQN,YAIjEqB,eAAeX,GACb,IAAIA,EAAEY,SAAWZ,EAAEa,UAA0B,IAAbb,EAAEc,QAAe,CAC/Cd,EAAEe,iBACF,IAAIC,EAAOhB,EAAEG,OAAOc,QAAQ,QAC5BpC,KAAKqC,WAAWF,IAqBpBE,WAAWF,GAGLA,EAAKG,cACPH,EAAKG,gBAELH,EAAKN,cAAc,iBAAiBU,WAjDjC3B,QAAU,CAAC,YAAa,0BCFJd,EAQ3B0C,SACE,MAAMC,EAAYzC,KAAK0C,gBAAiB1C,KAAK2C,mBAAoB3C,KAAK4C,mBAClE5C,KAAK0C,iBACP1C,KAAK6C,cAGP7C,KAAK8C,cAAcC,cAAc,IAAIC,YAAYP,IAG/CC,sBACF,YAAYI,cAAcG,UAAUC,SAASlD,KAAKmD,aAGpDN,cACE7C,KAAK8C,cAAcG,UAAUG,OAAOpD,KAAKmD,aAG3CE,cACErD,KAAK8C,cAAcG,UAAUK,IAAItD,KAAKmD,gBAzBjCvC,QAAU,CAAE,aACZ2C,QAAU,CAAE,YACZC,OAAS,CACdC,cAAeC,OACfC,cAAeD,QCDNE,MAAAA,EAAwB,CACnC,CAACC,EAAqB,2BACtB,CAACC,EAAgB,sBACjB,CAACC,EAAsB,8BACvBC,IAAI,SAASC,GACb,MACMC,EAAaD,EAAE,GACrB,MAAO,CACLE,WAAY1E,EAHFwE,EAAE,IAIZG,sBAAuBF,KCf3B5D,SAAS+D,iBAAiB,aAAc,KAClCC,UAAUC,UAAUC,oBAAoBC,SAAS,aACnDnE,SAASoE,KAAKzB,UAAUK,IAAI"}
|
@@ -5,6 +5,10 @@ module Account::Controllers::Base
|
|
5
5
|
include LoadsAndAuthorizesResource
|
6
6
|
include Fields::ControllerSupport
|
7
7
|
|
8
|
+
if billing_enabled?
|
9
|
+
include Billing::ControllerSupport
|
10
|
+
end
|
11
|
+
|
8
12
|
before_action :set_last_seen_at, if: proc {
|
9
13
|
user_signed_in? && (current_user.last_seen_at.nil? || current_user.last_seen_at < 1.minute.ago)
|
10
14
|
}
|
@@ -106,6 +110,11 @@ module Account::Controllers::Base
|
|
106
110
|
end
|
107
111
|
end
|
108
112
|
|
113
|
+
# TODO Maybe in this context we should check whether `Billing::ControllerSupport` is included instead of just defined?
|
114
|
+
if defined?(Billing::ControllerSupport)
|
115
|
+
enforce_billing_requirements
|
116
|
+
# See `app/controllers/concerns/billing_support.rb` for details.
|
117
|
+
end
|
109
118
|
end
|
110
119
|
|
111
120
|
true
|
data/app/helpers/base_helper.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
module BaseHelper
|
2
2
|
# TODO This is for the billing package to override, but I feel like there has got to be a better way to do this.
|
3
3
|
def hide_team_resource_menus?
|
4
|
-
|
4
|
+
if billing_enabled?
|
5
|
+
current_team.needs_billing_subscription?
|
6
|
+
else
|
7
|
+
false
|
8
|
+
end
|
5
9
|
end
|
6
10
|
end
|
@@ -29,6 +29,11 @@ module Memberships::Base
|
|
29
29
|
scope :current_and_invited, -> { includes(:invitation).where("user_id IS NOT NULL OR invitations.id IS NOT NULL").references(:invitation) }
|
30
30
|
scope :current, -> { where("user_id IS NOT NULL") }
|
31
31
|
scope :tombstones, -> { includes(:invitation).where("user_id IS NULL AND invitations.id IS NULL").references(:invitation) }
|
32
|
+
|
33
|
+
# TODO Probably we can provide a way for gem packages to define these kinds of extensions.
|
34
|
+
if billing_enabled?
|
35
|
+
scope :billable, -> { current }
|
36
|
+
end
|
32
37
|
end
|
33
38
|
|
34
39
|
def name
|
@@ -28,6 +28,14 @@ module Records::Base
|
|
28
28
|
scope :newest_updated, -> { order("updated_at DESC") }
|
29
29
|
scope :oldest_updated, -> { order("updated_at ASC") }
|
30
30
|
|
31
|
+
# TODO Probably we can provide a way for gem packages to define these kinds of extensions.
|
32
|
+
if billing_enabled?
|
33
|
+
# By default, any model in a collection is considered active for billing purposes.
|
34
|
+
# This can be overloaded in the child model class to specify more specific criteria for billing.
|
35
|
+
# See `app/models/concerns/memberships/base.rb` for an example.
|
36
|
+
scope :billable, -> { order("TRUE") }
|
37
|
+
end
|
38
|
+
|
31
39
|
# Microscope adds useful scopes targeting ActiveRecord `boolean`, `date` and `datetime` attributes.
|
32
40
|
# https://github.com/mirego/microscope
|
33
41
|
acts_as_microscope
|
@@ -18,6 +18,17 @@ module Teams::Base
|
|
18
18
|
# integrations
|
19
19
|
has_many :integrations_stripe_installations, class_name: "Integrations::StripeInstallation", dependent: :destroy if stripe_enabled?
|
20
20
|
|
21
|
+
# TODO Probably we can provide a way for gem packages to define these kinds of extensions.
|
22
|
+
if billing_enabled?
|
23
|
+
# subscriptions
|
24
|
+
has_many :billing_subscriptions, class_name: "Billing::Subscription", dependent: :destroy, foreign_key: :team_id
|
25
|
+
|
26
|
+
# TODO We need a way for `bullet_train-billing-stripe` to define these.
|
27
|
+
if defined?(Billing::Stripe::Subscription)
|
28
|
+
has_many :billing_stripe_subscriptions, class_name: "Billing::Stripe::Subscription", dependent: :destroy, foreign_key: :team_id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
21
32
|
# validations
|
22
33
|
validates :name, presence: true
|
23
34
|
validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
|
@@ -48,4 +59,12 @@ module Teams::Base
|
|
48
59
|
# generic functions need to function for a team model as well, so we do this.
|
49
60
|
self
|
50
61
|
end
|
62
|
+
|
63
|
+
# TODO Probably we can provide a way for gem packages to define these kinds of extensions.
|
64
|
+
if billing_enabled?
|
65
|
+
def needs_billing_subscription?
|
66
|
+
return false if freemium_enabled?
|
67
|
+
billing_subscriptions.active.empty?
|
68
|
+
end
|
69
|
+
end
|
51
70
|
end
|
@@ -32,7 +32,7 @@
|
|
32
32
|
<meta property="og:url" content="<%= request.base_url + request.path %>" />
|
33
33
|
<meta property="og:description" content="<%= description.truncate(200) %>" />
|
34
34
|
</head>
|
35
|
-
<body class="bg-light-
|
35
|
+
<body class="bg-light-gradient text-gray-700 text-sm font-normal dark:bg-dark-gradient dark:text-darkPrimary-300">
|
36
36
|
<div class="md:p-5">
|
37
37
|
<div class="h-screen md:h-auto overflow-hidden md:rounded-lg flex shadow"
|
38
38
|
data-controller="mobile-menu"
|
@@ -295,7 +295,7 @@
|
|
295
295
|
data-transition-leave-start="translate-x-0"
|
296
296
|
data-transition-leave-end="-translate-x-full"
|
297
297
|
|
298
|
-
class="relative flex-1 flex flex-col max-w-xs w-full
|
298
|
+
class="relative flex-1 flex flex-col max-w-xs w-full pb-4 bg-dark-gradient shadow-xl"
|
299
299
|
>
|
300
300
|
<%= menu %>
|
301
301
|
</div>
|
@@ -303,13 +303,13 @@
|
|
303
303
|
</div>
|
304
304
|
</div>
|
305
305
|
|
306
|
-
<div class="hidden lg:flex lg:flex-shrink-0 bg-gradient-to-b from-
|
306
|
+
<div class="hidden lg:flex lg:flex-shrink-0 overflow-y-auto bg-gradient-to-b from-primary-700 to-primary-800 dark:from-darkPrimary-800 dark:to-darkPrimary-800">
|
307
307
|
<div class="w-64">
|
308
308
|
<%= menu %>
|
309
309
|
</div>
|
310
310
|
</div>
|
311
311
|
|
312
|
-
<div class="flex flex-col w-0 flex-1 overflow-
|
312
|
+
<div class="flex flex-col w-0 flex-1 overflow-y-auto bg-gray-100 dark:bg-darkPrimary-800 lg:border-l dark:border-gray-500">
|
313
313
|
<main class="flex-1 relative z-0 overflow-y-auto focus:outline-none" tabindex="0">
|
314
314
|
|
315
315
|
<button class="lg:hidden h-12 w-12 ml-1 flex-none inline-flex items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
en:
|
2
|
+
billing/products:
|
3
|
+
free:
|
4
|
+
name: Free
|
5
|
+
basic:
|
6
|
+
name: Basic
|
7
|
+
description: This is an example plan that includes a free trial when paid for monthly.
|
8
|
+
features:
|
9
|
+
- Demonstrates pricing per team member.
|
10
|
+
- Allows creation of up to fifty "Creative Concepts".
|
11
|
+
- Soft enforcement that limit.
|
12
|
+
pro:
|
13
|
+
name: Pro
|
14
|
+
description: An improved example plan that demonstrates different features.
|
15
|
+
features:
|
16
|
+
- Demonstrates a fixed price for up to ten team members.
|
17
|
+
- Allows creation of an unlimited number of "Creative Concepts".
|
data/docs/billing/stripe.md
CHANGED
@@ -1,81 +1,122 @@
|
|
1
|
-
#
|
1
|
+
# Bullet Train Billing for Stripe
|
2
2
|
|
3
|
-
Bullet Train
|
3
|
+
When you're ready to start billing customers for the product you've created with Bullet Train, you can take advantage of our streamlined, commercial billing package that includes a traditional SaaS pricing page powered by Yaml configuration for products and prices.
|
4
4
|
|
5
|
-
|
5
|
+
We also provide a Stripe-specific adapter package with support for auto-configuring those products and prices in your Stripe account. It also takes advantage of completely modern Stripe workflows, like allowing customers to purchase your product with Stripe Checkout and later manage their subscription using Stripe Billing's customer portal. It also automatically handles incoming Stripe webhooks as well, to keep subscription state in your application up-to-date with activity that has happened on Stripe's platform.
|
6
6
|
|
7
|
-
|
7
|
+
## 1. Purchase Bullet Train Billing for Stripe
|
8
8
|
|
9
|
-
|
9
|
+
First, [purchase Bullet Train Billing for Stripe](https://buy.stripe.com/28o8zg4dBbrd59u7sM). Once you've completed this process, you'll be issued a private token for the Bullet Train Pro package server. (This process is currently completed manually, so please be patient.)
|
10
|
+
|
11
|
+
## 2. Install the Package
|
12
|
+
|
13
|
+
### 2.1. Add the Private Ruby Gems
|
14
|
+
|
15
|
+
You'll need to specify both Ruby gems in your `Gemfile`, since we have to specify a private source for both:
|
16
|
+
|
17
|
+
```
|
18
|
+
source "https://YOUR_TOKEN_HERE@gem.fury.io/bullettrain" do
|
19
|
+
gem "bullet_train-billing"
|
20
|
+
gem "bullet_train-billing-stripe"
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
### 2.2. Bundle Install
|
25
|
+
|
26
|
+
```
|
27
|
+
bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
### 2.3. Copy Database Migrations
|
31
|
+
|
32
|
+
Use the following two commands on your shell to copy the required migrations into your local project:
|
33
|
+
|
34
|
+
```
|
35
|
+
cp `bundle show --paths | grep bullet_train-billing | sort | head -n 1`/db/migrate/* db/migrate
|
36
|
+
cp `bundle show --paths | grep bullet_train-billing-stripe | sort | head -n 1`/db/migrate/* db/migrate
|
37
|
+
```
|
38
|
+
|
39
|
+
Note this is different than how many Rails engines ask you to install migrations. This is intentional, as we want to maintain the original timestamps associated with these migrations.
|
40
|
+
|
41
|
+
### 2.4. Run Migrations
|
10
42
|
|
11
43
|
```
|
12
44
|
rake db:migrate
|
13
45
|
```
|
14
46
|
|
15
|
-
## 3.
|
47
|
+
## 3. Configure Your Products
|
48
|
+
|
49
|
+
Bullet Train defines subscription plans and pricing options in `config/models/billing/products.yml` and defines the translatable elements of these plans in `config/locales/en/billing/products.en.yml`. We recommend just getting started with these plans to ensure your setup is working before customizing the attributes of these plans.
|
50
|
+
|
51
|
+
## 4. Configure Stripe
|
52
|
+
|
53
|
+
### 4.1. Create API Keys with Stripe
|
16
54
|
|
17
55
|
- Create a Stripe account if you don't already have one.
|
18
|
-
- Visit https://dashboard.stripe.com/apikeys.
|
19
|
-
- For your development environment, make sure you toggle the "test mode" flag to "on" in the top-right corner.
|
56
|
+
- Visit https://dashboard.stripe.com/test/apikeys.
|
20
57
|
- Create a new secret key.
|
21
58
|
|
22
|
-
|
59
|
+
**Note:** By default we're linking to the "test mode" page for API keys so you can get up and running in development. When you're ready to deploy to production, you'll have to repeat this step and toggle the "test mode" option off to provision real API keys for live payments.
|
23
60
|
|
24
|
-
|
61
|
+
### 4.2. Configure Stripe API Keys Locally
|
62
|
+
|
63
|
+
Edit `config/application.yml` and add your new Stripe secret key to the file:
|
25
64
|
|
26
65
|
```
|
27
|
-
BILLING_DEFAULT_SUBSCRIPTION: "Billing::Stripe::Subscription"
|
28
|
-
STRIPE_PUBLISHABLE_KEY: pk_0CJwz5wHlKBXxDA4VO1uEoipxQob0
|
29
66
|
STRIPE_SECRET_KEY: sk_0CJw2Iu5wwIKXUDdqphrt2zFZyOCH
|
30
67
|
```
|
31
68
|
|
32
|
-
|
33
|
-
|
34
|
-
Bullet Train defines subscription plans and other purchasable add-ons in `config/models/billing/products.yml` and comes preconfigured with some example plans. We recommend just getting started with these plans to ensure your setup is working before customizing the attributes of these plans.
|
69
|
+
### 4.3. Populate Stripe with Locally Configured Products
|
35
70
|
|
36
|
-
Before you can use Stripe Checkout or Stripe Billing's customer portal,
|
71
|
+
Before you can use Stripe Checkout or Stripe Billing's customer portal, your locally configured products will have to be created on Stripe as well. To accomplish this, you can have all locally defined products automatically created on Stripe via API by running the following:
|
37
72
|
|
38
73
|
```
|
39
74
|
rake billing:stripe:populate_products_in_stripe
|
40
75
|
```
|
41
76
|
|
42
|
-
|
77
|
+
### 4.4. Add Additional Environment Variables
|
43
78
|
|
44
79
|
The script in the previous step will output some additional environment variables you need to copy into `config/application.yml`.
|
45
80
|
|
46
|
-
## 7. Restart Rails
|
47
81
|
|
48
|
-
|
82
|
+
## 5. Wire Up Webhooks
|
49
83
|
|
50
|
-
|
51
|
-
rails restart
|
52
|
-
```
|
84
|
+
Basic subscription creation will work without receiving and processing Stripe's webhooks. However, advanced payment workflows like SCA payments and customer portal cancelations and plan changes require receiving webhooks and processing them.
|
53
85
|
|
54
|
-
|
86
|
+
### 5.1. Ensure HTTP Tunneling is Enabled
|
55
87
|
|
56
|
-
|
88
|
+
Although Stripe provides free tooling for receiving webhooks in your local environment, the officially supported mechanism for doing so in Bullet Train is using [HTTP Tunneling with ngrok](/docs/tunneling.md). This is because we provide support for many types of webhooks across different platforms and packages, so we already need to have ngrok in play.
|
57
89
|
|
58
|
-
|
90
|
+
Ensure you've completed the steps from [HTTP Tunneling with ngrok](/docs/tunneling.md), including updating `BASE_URL` in `config/application.yml` and restarting your Rails server.
|
59
91
|
|
60
|
-
|
92
|
+
### 5.2. Enable Stripe Webhooks
|
61
93
|
|
62
|
-
Basic subscription creation will work without receiving and processing Stripe's webhooks. However, advanced payment workflows like SCA payments and customer portal cancelations and plan changes require receiving webhooks and processing them.
|
63
|
-
|
64
|
-
- Stripe can't deliver webhooks to `http://localhost:3000`, so you'll need to [get an HTTP tunnel up and running](/docs/tunneling.md). For this example, we'll assume you're using ngrok.
|
65
94
|
- Visit https://dashboard.stripe.com/test/webhooks/create.
|
66
|
-
-
|
67
|
-
-
|
68
|
-
-
|
69
|
-
-
|
95
|
+
- Use the default "add an endpoint" form.
|
96
|
+
- Set "endpoint URL" to `https://YOUR-SUBDOMAIN.ngrok.io/webhooks/incoming/stripe_webhooks`.
|
97
|
+
- Under "select events to listen to" choose "select all events" and click "add events".
|
98
|
+
- Finalize the creation of the endpoint by clicking "add endpoint".
|
99
|
+
|
100
|
+
### 5.3. Configure Stripe Webhooks Signing Secret
|
101
|
+
|
102
|
+
After creating the webhook endpoint, click "reveal" under the heading "signing secret". Copy the `whsec_...` value into your `config/application.yml` like so:
|
103
|
+
|
104
|
+
```
|
105
|
+
STRIPE_WEBHOOKS_ENDPOINT_SECRET: whsec_vchvkw3hrLK7SmUiEenExipUcsCgahf9
|
106
|
+
```
|
70
107
|
|
71
|
-
|
72
|
-
STRIPE_WEBHOOKS_ENDPOINT_SECRET: whsec_VsM3c2zeZyqAddkaPaXzf1wJsYp2fRKR
|
73
|
-
```
|
108
|
+
### 5.4. Test Sample Webhook Delivery
|
74
109
|
|
75
110
|
- Restart your Rails server with `rails restart`.
|
76
111
|
- Trigger a test webhook just to ensure it's resulting in an HTTP status code of 201.
|
77
112
|
|
78
|
-
##
|
113
|
+
## 6. Test Creating a Subscription
|
114
|
+
|
115
|
+
Bullet Train comes preconfigured with a "freemium" plan, so new and existing accounts will continue to work as normal. A new "billing" menu item will appear and you can test subscription creation by clicking "upgrade" and selecting one of the two plans presented.
|
116
|
+
|
117
|
+
You should be in "test mode" on Stripe, so when prompted for a credit card number, you can enter `4242 4242 4242 4242`.
|
118
|
+
|
119
|
+
## 7. Configure Stripe Billing's Customer Portal
|
79
120
|
|
80
121
|
- Visit https://dashboard.stripe.com/test/settings/billing/portal.
|
81
122
|
- Complete all required fields.
|
@@ -83,8 +124,23 @@ Basic subscription creation will work without receiving and processing Stripe's
|
|
83
124
|
|
84
125
|
This "products" list is what Stripe will display to users as upgrade and downgrade options in the customer portal. You shouldn't list any products here that aren't properly configured in your Rails app, otherwise the resulting webhook will fail to process. If you want to stop offering a plan, you should remove it from this list as well.
|
85
126
|
|
86
|
-
##
|
127
|
+
## 8. Finalize Webhooks Testing by Managing a Subscription
|
87
128
|
|
88
129
|
In the same account where you created your first test subscription, go into the "billing" menu and click "manage" on that subscription. This will take you to the Stripe Billing customer portal.
|
89
130
|
|
90
131
|
Once you're in the customer portal, you should test upgrading, downgrading, and canceling your subscription and clicking "⬅ Return to {Your Application Name}" in between each step to ensure that each change you're making is properly reflected in your Bullet Train application. This will let you know that webhooks are being properly delivered and processed and all the products in both systems are properly mapped in both directions.
|
132
|
+
|
133
|
+
## 9. Rinse and Repeat Configuration Steps for Production
|
134
|
+
|
135
|
+
As mentioned earlier, all of the links we provided for configuration steps on Stripe were linked to the "test mode" on your Stripe account. When you're ready to launch payments in production, you will need to:
|
136
|
+
|
137
|
+
- Complete all configuration steps again in the live version of your Stripe account. You can do this by following all the links in the steps above and toggling the "test mode" switch to visit the live mode version of each page.
|
138
|
+
- After creating a live API key, configure `STRIPE_SECRET_KEY` in your production environment.
|
139
|
+
- Run `STRIPE_SECRET_KEY=... rake billing:stripe:populate_products_in_stripe` (where `...` is your live secret key) in order to create live versions of your products and prices.
|
140
|
+
- Copy the environment variables output by that rake task into your production environment.
|
141
|
+
- Configure a live version of your webhooks endpoint for the production environment by following the same steps as before, but replacing the ngrok host with your production host in the endpoint URL.
|
142
|
+
- After creating the live webhooks endpoint, configure the corresponding signing secret as the `STRIPE_WEBHOOKS_ENDPOINT_SECRET` enviornment variable in your production environment.
|
143
|
+
|
144
|
+
## 10. You should be done!
|
145
|
+
|
146
|
+
[Let us know on Discord](http://discord.gg/bullettrain) if any part of this guide was not clear or could be improved!
|
data/docs/index.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Bullet Train Developer Documentation
|
2
2
|
|
3
|
-
>
|
3
|
+
> Some of the open-source Bullet Train packages are considered pre-release, so please pardon the dust while we work to complete the documentation for certain topics.
|
4
4
|
|
5
5
|
## Introduction
|
6
6
|
- [What is Bullet Train?](https://bullettrain.co) <i class="ti ti-new-window ml-2"></i>
|
data/docs/tunneling.md
CHANGED
@@ -4,14 +4,14 @@ Before your application can take advantage of features that depend on incoming w
|
|
4
4
|
|
5
5
|
## Use a Paid Plan
|
6
6
|
|
7
|
-
You should specifically sign up for a paid account. Although ngrok offers a free plan, their $
|
7
|
+
You should specifically sign up for a paid account. Although ngrok offers a free plan, their $25/month paid plan will allow you to reserve a custom subdomain for reuse each time you spin up your tunnel. This is a critical productivity improvement, because in practice you'll end up configuring your tunnel URL in a bunch of different places like `config/application.yml` but also in external systems like when you [configure payment providers to deliver webhooks to you](docs/billing/stripe.md).
|
8
8
|
|
9
9
|
## Usage
|
10
10
|
|
11
|
-
Once you have ngrok installed, you can start your tunnel like so, replacing `
|
11
|
+
Once you have ngrok installed, you can start your tunnel like so, replacing `YOUR-SUBDOMAIN` with whatever subdomain you reserved in your ngrok account:
|
12
12
|
|
13
13
|
```
|
14
|
-
ngrok http 3000 -subdomain=
|
14
|
+
ngrok http 3000 -subdomain=YOUR-SUBDOMAIN
|
15
15
|
```
|
16
16
|
|
17
17
|
## Updating Your Configuration
|
@@ -19,7 +19,7 @@ ngrok http 3000 -subdomain=your-subdomain
|
|
19
19
|
Before your Rails application will accept connections on your tunnel hostname, you need to update `config/application.yml` with:
|
20
20
|
|
21
21
|
```
|
22
|
-
BASE_URL: https://
|
22
|
+
BASE_URL: https://YOUR-SUBDOMAIN.ngrok.io
|
23
23
|
```
|
24
24
|
|
25
25
|
You'll also need to restart your Rails server:
|
data/lib/bullet_train/version.rb
CHANGED
data/lib/bullet_train.rb
CHANGED
@@ -61,7 +61,12 @@ def inbound_email_enabled?
|
|
61
61
|
ENV["INBOUND_EMAIL_DOMAIN"].present?
|
62
62
|
end
|
63
63
|
|
64
|
-
def
|
64
|
+
def billing_enabled?
|
65
|
+
defined?(BulletTrain::Billing)
|
66
|
+
end
|
67
|
+
|
68
|
+
# TODO This should be in an initializer or something.
|
69
|
+
def billing_subscription_creation_disabled?
|
65
70
|
false
|
66
71
|
end
|
67
72
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bullet_train
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.86
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Culver
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-06-
|
11
|
+
date: 2022-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: standard
|
@@ -428,6 +428,8 @@ files:
|
|
428
428
|
- README.md
|
429
429
|
- Rakefile
|
430
430
|
- app/assets/config/bullet_train_manifest.js
|
431
|
+
- app/assets/javascripts/bullet-train.js
|
432
|
+
- app/assets/javascripts/bullet-train.js.map
|
431
433
|
- app/controllers/account/invitations_controller.rb
|
432
434
|
- app/controllers/account/memberships_controller.rb
|
433
435
|
- app/controllers/account/onboarding/user_details_controller.rb
|
@@ -556,6 +558,7 @@ files:
|
|
556
558
|
- config/initializers/concerns/inflections_base.rb
|
557
559
|
- config/initializers/concerns/turbo_failure_app.rb
|
558
560
|
- config/locales/en/base.yml
|
561
|
+
- config/locales/en/billing/products.en.yml
|
559
562
|
- config/locales/en/devise.en.yml
|
560
563
|
- config/locales/en/doorkeeper.en.yml
|
561
564
|
- config/locales/en/invitations.en.yml
|
@@ -665,7 +668,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
665
668
|
- !ruby/object:Gem::Version
|
666
669
|
version: '0'
|
667
670
|
requirements: []
|
668
|
-
rubygems_version: 3.
|
671
|
+
rubygems_version: 3.3.7
|
669
672
|
signing_key:
|
670
673
|
specification_version: 4
|
671
674
|
summary: Bullet Train
|