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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e07a863ffd021a1e4da0bf8e18e68fb9e357d90264d5a0d0d92770363f48390c
4
- data.tar.gz: be20709eb91a25d64c9ef4371326cb56cc9b938b34bbb2e80db8ad21396d085c
3
+ metadata.gz: 849a48985baf2ad6e8e2fec56d4fabaad5bf1218012ee46c3af717b6c2328a61
4
+ data.tar.gz: ca355a5d67e5caf57159e4dcf9592b375bd492d96e53783c753a75d1230c0dc7
5
5
  SHA512:
6
- metadata.gz: 1f887941fdeb2c3cb567b7011a1ff7edbf47558bc362d4b714c8a0d6ea6d4da922ac4d3eccc16c01c384ed9773eacc3f1b84ed3aadcf41adc7fd5d692b9ff0b4
7
- data.tar.gz: bd70518110d78e2c7d455fa314a70a874fc6d82b109c28188098b7ef91701af31c85ae9eb7be5f1dbff67b95e8c91005c4babb89179b2cbbc2bda7a0bd38254a
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(/&nbsp;<\/div>$/)?(e.element.innerHTML=e.element.innerHTML.slice(0,-12)+"</div>",this.removeTrailingWhitespace(e)):e.element.innerHTML.match(/&nbsp; <\/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(/&nbsp;<\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -12) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n } else if (trixEditor.element.innerHTML.match(/&nbsp; <\\/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
@@ -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
- false
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-blue-gradient text-gray-700 text-sm font-normal dark:bg-dark-blue-gradient dark:text-sealBlue-900">
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 shadow-xl bg-gradient-to-b from-vividBlue-700 to-vividBlue-800 dark:from-sealBlue-200 dark:to-sealBlue-200"
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-vividBlue-700 to-vividBlue-800 dark:from-sealBlue-200 dark:to-sealBlue-200">
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-hidden bg-gray-100 dark:bg-sealBlue-200 lg:border-l dark:border-gray-500">
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".
@@ -1,81 +1,122 @@
1
- # Enabling Stripe Subscriptions
1
+ # Bullet Train Billing for Stripe
2
2
 
3
- Bullet Train provides a base billing package and a Stripe-specific package with support for Stripe Checkout, Stripe Billing's customer portal, and incoming Stripe webhooks.
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
- ## 1. Add the `bullet_train-billing-stripe` Ruby gem.
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
- > TODO Add instructions about subscribing to Bullet Train Pro.
7
+ ## 1. Purchase Bullet Train Billing for Stripe
8
8
 
9
- ## 2. Migrate the Database
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. Create API Keys with Stripe
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
- ## 4. Configure Stripe API Keys Locally
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
- Edit `config/application.yml` and add your Stripe publishable key and new secret key to the file, and also tell the system to use Stripe subscriptions by default:
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
- ## 5. Populate Stripe with Locally Configured Products
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, these products will have to be defined on Stripe as well. You can have all locally defined products automatically created on Stripe by running the following:
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
- ## 6. Import Additional Environment Variables
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
- We've modified a bunch of environment variables, so you'll have to have to restart your Rails server before you see the results in your browser.
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
- ## 8. Test Creating a Subscription
86
+ ### 5.1. Ensure HTTP Tunneling is Enabled
55
87
 
56
- 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.
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
- You should be in "test mode" on Stripe, so when prompted for a credit card number, you can enter `4242 4242 4242 4242`.
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
- ## 9. Configuring Webhooks
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
- - Configure the "endpoint URL" to be `https://your-tunnel.ngrok.io/webhooks/incoming/stripe_webhooks`, replacing `your-tunnel` with whatever the subdomain of your tunnel is.
67
- - When configuring which events to receive, just "select all events" for simplicity. This ensures that any webhooks Bullet Train might add support for in the future will be properly handled when you upgrade.
68
- - Add the endpoint.
69
- - On the page for the webhook endpoint you've just configured with Stripe, click on "reveal" under the heading "signing secret". This is a secret token that is required to authenticate that webhooks your application is receiving are actually coming from Stripe. Copy this into your `config/application.yml` like so:
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
- ## 10. Configure Stripe Billing's Customer Portal
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
- ## 11. Test Webhooks by Managing a Subscription
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
- > This release of Bullet Train is still a pre-release, so please pardon the dust while we work to complete the documentation for certain topics.
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 $5/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).
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 `your-subdomain` with whatever subdomain you reserved in your ngrok account:
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=your-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://your-subdomain.ngrok.io
22
+ BASE_URL: https://YOUR-SUBDOMAIN.ngrok.io
23
23
  ```
24
24
 
25
25
  You'll also need to restart your Rails server:
@@ -1,3 +1,3 @@
1
1
  module BulletTrain
2
- VERSION = "1.0.81"
2
+ VERSION = "1.0.86"
3
3
  end
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 subscriptions_enabled?
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.81
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-16 00:00:00.000000000 Z
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.2.22
671
+ rubygems_version: 3.3.7
669
672
  signing_key:
670
673
  specification_version: 4
671
674
  summary: Bullet Train