It adds support to Devise for sending invitations by email (it requires to be authenticated) and accept the invitation setting the password.
The latest version of DeviseInvitable requires Ruby >= 2.7, Rails >= 7.0, and Devise >= 5.0.
Use DeviseInvitable 2.x with Devise 4.6 through 4.9. For earlier Devise 4 releases, use DeviseInvitable 1.7.5.
Install DeviseInvitable gem:
gem install devise_invitableAdd DeviseInvitable to your Gemfile:
gem 'devise_invitable', '~> 3.0'Run the following generator to add DeviseInvitable’s configuration option in
the Devise configuration file (config/initializers/devise.rb):
rails generate devise_invitable:installWhen you are done, you are ready to add DeviseInvitable to any of your Devise models using the following generator:
rails generate devise_invitable MODELReplace MODEL by the class name you want to add DeviseInvitable, like User,
Admin, etc. This will add the :invitable flag to your model's Devise
modules. The shared invitations table is created by the install generator, so
the model generator does not create a migration.
Follow the walkthrough for Devise and after it's done, follow this walkthrough.
Add :invitable to the devise call in your model (we’re assuming here you
already have a User model with some Devise modules):
class User < ActiveRecord::Base
devise :database_authenticatable, :confirmable, :invitable
endInvitations are stored in the shared devise_invitations table generated by
devise_invitable:install. It contains the token and lifecycle timestamps,
the configured invite keys, and polymorphic invitable and invited_by
associations. Invitation columns are not added to each Devise model.
Run the generated migration:
rails db:migrateGenerate one upgrade migration for every model that already uses
:invitable:
rails generate devise_invitable:upgrade User
rails db:migratePass every configured invite key when it is not just email:
rails generate devise_invitable:upgrade User --invite-keys email usernameThe generated migration creates the shared table if necessary, copies existing
invitation data, and removes the old invitation lifecycle and invited_by
columns. It intentionally leaves invitation_limit and invitations_count
alone because those describe inviters rather than individual invitations. Its
down direction is irreversible because multiple invitations cannot be moved
losslessly back onto one resource row.
Mongoid fields and indexes for DeviseInvitable::Invitation are defined by the
gem. Create the indexes after deployment:
rake db:mongoid:create_indexesTo migrate legacy embedded invitation fields for a model, run:
rails devise_invitable:migrate_invitations[User]DeviseInvitable adds some new configuration options:
invite_for: The period the generated invitation token is valid. After this period, the invited resource won't be able to accept the invitation. Wheninvite_foris0(the default), the invitation won't expire.
You can set this configuration option in the Devise initializer as follow:
# ==> Configuration for :invitable
# The period the generated invitation token is valid.
# After this period, the invited resource won't be able to accept the invitation.
# When invite_for is 0 (the default), the invitation won't expire.
# config.invite_for = 2.weeksor directly as parameters to the devise method:
devise :database_authenticatable, :confirmable, :invitable, invite_for: 2.weeks-
invitation_limit: The number of invitations users can send. The default value ofnilmeans users can send as many invites as they want, there is no limit for any user,invitation_limitcolumn is not used. A setting of0means they can't send invitations. A settingn > 0means they can sendninvitations. You can changeinvitation_limitcolumn for some users so they can send more or less invitations, even with globalinvitation_limit = 0. This setting controls how many invitations an inviter may send; useauthenticate_inviter!for authorization and other application-specific conditions. -
invite_key: The key to be used to check existing users when sending an invitation. You can use multiple keys. This value must be a hash with the invite key as hash keys, and values that respond to the===operator (including procs and regexes). The default value is looking for users by email and validating withDevise.email_regexp. -
validate_on_invite: with placeholder creation enabled, force the resource to be valid before it is invited. Deferred invitations validate their invite keys because no resource is persisted yet. -
resend_invitation: resend invitation if user with invited status is invited again. Enabled by default. -
invited_by_class_nameandinvited_by_foreign_keyare deprecated and have no effect. This applies to both initializer configuration and options passed todevise :invitableon a model. Invitations always use the polymorphicinvited_by_idandinvited_by_typecolumns in the shared invitations table. -
create_resource_on_invitation: create a placeholder resource when the invitation is sent. It is disabled by default, so only the invitation record is persisted while invite keys remain in the encrypted acceptance token. Enable it when the application needs an addressable resource before signup or needs to store additional permitted resource attributes from the invitation form. Arbitrary resource attributes are rejected when this is disabled. -
store_invite_keys_until_delivery: temporarily store invite keys in the invitation record so a skipped, resource-less invitation can be loaded and delivered later. It is disabled by default. Stored keys are cleared after successful delivery, when they are available in the encrypted acceptance token. -
deliver_invitation_later: enqueue invitation emails with the configured invitation delivery job. It is disabled by default, preserving the normalsend_devise_notificationhook. -
invitation_delivery_job: job class used byInvitation#deliver_later. It may be a class or class name and must respond toperform_later. The default isDeviseInvitable::InvitationDeliveryJob. -
invitation_delivery_queue_name: queue used by the default invitation delivery job. The default is:mailers. -
supersede_other_invitations_on_accept: supersede other pending invitations for the same invite keys when one is accepted. Enabled by default. Disable it only when each invitation carries independently acceptable application context. Remaining invitations are linked to the provisioned resource and their tokens stay valid; accepting them does not require or change the resource's password. -
invited_by_counter_cache: the column name used for counter_cache column. If this isnil(default value), theinvited_byassociation is declared withoutcounter_cache. -
allow_insecure_sign_in_after_accept: automatically sign in the user after they set a password. Enabled by default. -
require_password_on_accepting: require password when user accepts the invitation. Enabled by default. Disable if you don't want to ask or enforce to set password while accepting, because is set when user is invited or it will be set later.
For more details, see config/initializers/devise.rb (after you invoked the
devise_invitable:install generator described above).
All the views are packaged inside the gem. If you'd like to customize the views, invoke the following generator and it will copy all the views to your application:
rails generate devise_invitable:viewsYou can also use the generator to generate scoped views:
rails generate devise_invitable:views usersThen turn scoped views on in config/initializers/devise.rb:
config.scoped_views = truePlease refer to Devise's README for more information about views.
To change the controller's behavior, create a controller that inherits from
Devise::InvitationsController. The available methods are: new, create,
edit, and update. Refer to the original controllers source
before editing any of these actions. Your
controller might now look something like this:
class Users::InvitationsController < Devise::InvitationsController
def update
if some_condition
redirect_to root_path
else
super
end
end
endNow just tell Devise that you want to use your controller, the controller
above is 'users/invitations', so our routes.rb would have this line:
devise_for :users, controllers: { invitations: 'users/invitations' }The invitations controller prepends its authentication, invitation-limit, and
token-loading callbacks. These callbacks may render or redirect and halt the
callback chain before an ordinary callback inherited from
ApplicationController runs. If application context must be available even
when one of these guards halts the request, prepend that setup in the custom
invitations controller. For example, an around callback can set and restore
the locale so flash messages created by the guards use the requested locale:
class Users::InvitationsController < Devise::InvitationsController
prepend_around_action :use_locale
private
def use_locale(&action)
I18n.with_locale(locale_from_request, &action)
end
def locale_from_request
params[:locale].presence_in(I18n.available_locales.map(&:to_s)) ||
I18n.default_locale
end
endThe same ordering rule applies to tenant selection, request tracing, and other setup that must run before Devise Invitable's controller guards.
be sure that you generate the views and put them into the controller that you generated, so for this example it would be:
rails generate devise_invitable:views usersTo change behaviour of inviting or accepting users, you can simply override two methods:
class Users::InvitationsController < Devise::InvitationsController
private
# This is called when creating invitation.
# It should return an instance of resource class.
def invite_resource
# skip sending emails on invite
super { |user| user.skip_invitation = true }
end
# This is called when accepting invitation.
# It should return an instance of resource class.
def accept_resource
resource = resource_class.accept_invitation!(update_resource_params)
# Report accepting invitation to analytics
Analytics.report('invite.accept', resource.id)
resource
end
endWhen you customize your own views, you may end up adding new attributes to forms. Rails 4 moved the parameter sanitization from the model to the controller, causing DeviseInvitable to handle this concern at the controller as well. Read about it in Devise README
There are just two actions in DeviseInvitable that allows any set of parameters to be passed down to the model, therefore requiring sanitization. Their names and the permited parameters by default are:
invite(Devise::InvitationsController#create) - Permits only the authentication keys (likeemail)accept_invitation(Devise::InvitationsController#update) - Permitsinvitation_tokenpluspasswordandpassword_confirmation.
Here is an example of the steps needed to add a first_name, last_name and role to invited Users.
Caution: Adding roles requires additional security measures, such as preventing a standard user from inviting an administrator. Implement appropriate access controls to ensure system security.
Note: These modifications can be applied directly in the InvitationsController if not needed for other Devise actions.
before_action :configure_permitted_parameters, if: :devise_controller?
protected
# Permit the new params here.
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:invite, keys: [:first_name, :last_name, :role])
endclass User < ApplicationRecord
has_many :models
enum role: {Role 1 Name: 0, Role 2 Name: 1, Role 3 Name: 2, etc...}
end<h2><%= t "devise.invitations.new.header" %></h2>
<%= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<% resource.class.invite_key_fields.each do |field| -%>
<div class="field">
<%= f.label field %><br />
<%= f.text_field field %>
</div>
<% end %>
<div class="field">
<%= f.label :first_name %>
<%= f.text_field :first_name %>
</div>
<div class="field">
<%= f.label :last_name %>
<%= f.text_field :last_name %>
</div>
<div class="field">
<%= f.label :role %>
<%= f.select :role, options_for_select(User.roles.map { |key, value| [key.humanize, key] }), {prompt: "Select Role"} %>
</div>
<div class="actions">
<%= f.submit t("devise.invitations.new.submit_button") %>
</div>
<% end %>
To send an invitation to a user, use the invite! class method. :email must
be present in the parameters hash. By default this stores an invitation without
creating a user. Set create_resource_on_invitation: true on the Devise model
to create a placeholder user immediately and allow other resource attributes.
User.invite!(email: 'new_user@example.com')
# => an invitation email will be sent to new_user@example.comTo persist additional resource attributes, enable placeholders:
class User < ApplicationRecord
devise :database_authenticatable, :invitable,
create_resource_on_invitation: true
end
User.invite!(email: 'new_user@example.com', name: 'John Doe')Multiple invitations can be sent for the same invite keys. Accepting one marks that invitation accepted and supersedes the others. If the invitee registers directly, all matching pending invitations are superseded rather than marked accepted.
Small application-specific values can be carried in the encrypted acceptance
token with the invitation_data option:
User.invite!(
{ email: 'new_user@example.com' },
current_user,
invitation_data: { organization_id: organization.id, role: 'member' }
)invitation_data must be a JSON-compatible hash whose serialized form is no
larger than 1,024 bytes. It is deliberately not persisted by devise_invitable.
After token decoding, JSON object keys are strings. The data is available as
current_invitation.invitation_data in invitation callbacks and controller
code. For example:
after_invitation_created do |user|
InvitationContext.create!(
devise_invitation: user.current_invitation,
data: user.current_invitation.invitation_data
)
end
after_invitation_accepted do |user|
organization_id = user.current_invitation.invitation_data['organization_id']
user.memberships.find_or_create_by!(organization_id: organization_id)
endPersist the context in an application model when it must survive independently of the email link, be processed after another invitation is accepted, or be combined with other invitations. Keep it URL-only when each invitation should be accepted separately. To preserve the other links in that case:
class User < ApplicationRecord
devise :database_authenticatable, :invitable,
supersede_other_invitations_on_accept: false
endThe first accepted invitation provisions the resource normally. Later
invitations run the acceptance callbacks with their own invitation_data, but
cannot replace or require the existing password and do not automatically sign
the resource in. Invitation data is a bearer credential; applications must
re-check referenced resources, authorization, and current roles when accepting
it.
Placeholder resources expose invitation_pending?. Pending placeholders are
rejected by both Devise's password validation and
active_for_authentication? checks until an invitation is accepted or the
resource is claimed by direct registration.
Invitation keys commonly contain personal data such as an email address. With
the default create_resource_on_invitation: false, no resource row is created,
and new invitations do not persist the invite-key values in the
devise_invitations.invite_key JSON column. The values are carried in the
encrypted and authenticated acceptance token and restored only when the link
is accepted. invite_key_digest is stored so the values in the token can be
verified against the invitation record. Invitations created before this change
may still have plaintext invite keys; their existing links remain supported.
With create_resource_on_invitation: true, the resource and any additional
attributes or associations supplied to invite! are persisted immediately.
Choose this mode only when the application needs a placeholder and its privacy
policy permits storing that data before acceptance. An application may clear
the placeholder's invite-key attributes after delivery, for example in an
after_invitation_created callback; the encrypted acceptance token restores
them during acceptance. The application is responsible for allowing the blank
placeholder attributes and persisting the cleanup.
invite_for limits token validity but does not delete invitation or resource
records. Applications are responsible for choosing a lawful basis for
processing, disclosing that processing where required, limiting invited data,
restricting access, protecting it at rest, and deleting pending, revoked,
superseded, and accepted invitation data according to their retention policy.
Consent is one possible lawful basis under GDPR, but it is not the only one.
See the European Commission GDPR principles
and obtain appropriate legal advice for the application.
The acceptance token is a bearer credential: avoid logging it or exposing it
to third parties. Its encryption key is derived from the Rails application's
key generator, so changing secret_key_base invalidates outstanding encrypted
invitation links.
If you want to create the invitation but not send it, you can set
skip_invitation to true.
user = User.invite!(email: 'new_user@example.com') do |u|
u.skip_invitation = true
end
# => the invitation will be stored, but its email will not be sentWhen generating the accept_user_invitation_url yourself, you must use the
raw_invitation_token. For new invitations this is an encrypted, authenticated
envelope containing the underlying Devise token and invite keys. It is
temporarily available on the object that creates the invitation and is not
persisted in plaintext.
accept_user_invitation_url(invitation_token: user.raw_invitation_token)When skip_invitation is used, the resulting invitation remains pending with
an unset invitation_sent_at. Deliver it later by calling deliver on the
invitation:
For resource-less invitations, first enable temporary invite-key storage:
devise :invitable, store_invite_keys_until_delivery: trueinvitation = user.current_invitation
invitation.deliverThis works for placeholder-backed invitations without additional configuration.
For a resource-less invitation, deliver reconstructs the resource from the
temporarily stored invite key. It generates a fresh encrypted acceptance token,
sends the email, clears the stored invite key, and sets invitation_sent_at.
Calling it again after successful delivery does nothing.
For example, an approval workflow can find unsent pending invitations and deliver an approved one after applying its own authorization policy:
invitations = DeviseInvitable::Invitation.pending.where(invitation_sent_at: nil)
invitation = invitations.find(approved_invitation_id)
invitation.deliverInvitation validity starts at invitation_sent_at, so time spent awaiting
approval does not reduce the configured invite_for period.
You can add :skip_invitation to attributes hash if skip_invitation is
added to attr_accessible.
User.invite!(email: 'new_user@example.com', skip_invitation: true)
# => the invitation will be stored, but its email will not be sentskip_invitation skips sending the email but still creates a pending
DeviseInvitable::Invitation. With placeholders enabled,
invited_to_sign_up? and invitation_pending? return true on the resource.
Devise Invitable includes a lazily loaded Active Job for background invitation delivery. To use it for the normal invitation flow, enable it in the Devise initializer:
config.deliver_invitation_later = trueUser.invite!(email: 'new_user@example.com')It can also enqueue an invitation that was created without sending its email:
user = User.invite!(email: 'new_user@example.com', skip_invitation: true)
user.current_invitation.deliver_laterThe default job runs on the mailers queue. It reconstructs a resource-less
invitee from the encrypted acceptance token and calls deliver_now on the
configured Devise mailer because it is already executing inside a background
job. It retains invitation_sent_at after successful delivery and leaves
failed invitations retryable by clearing that timestamp if delivery raises.
The example above does not require store_invite_keys_until_delivery because
the invitation is enqueued while its acceptance token is still available. If
an invitation will be reloaded and approved before it is enqueued, enable
temporary invite-key storage:
class User < ApplicationRecord
devise :invitable, store_invite_keys_until_delivery: true
end
invitation = DeviseInvitable::Invitation.find(approved_invitation_id)
invitation.deliver_laterStored invite keys are cleared after successful delivery. Placeholder-backed invitations do not need this setting because their persisted resource contains the delivery address.
Configure the default queue in the Devise initializer:
config.invitation_delivery_queue_name = :critical_mailersTo inherit from the application's ApplicationJob, add retries or callbacks,
or use another compatible job implementation, configure a custom job class:
class ApplicationInvitationDeliveryJob < ApplicationJob
queue_as :mailers
retry_on MailProviderError
def perform(invitation, acceptance_token = nil, options = {})
DeviseInvitable::InvitationDelivery.deliver_now(
invitation,
acceptance_token: acceptance_token,
options: options
)
end
endconfig.invitation_delivery_job = 'ApplicationInvitationDeliveryJob'By default, the normal invitation flow and synchronous Invitation#deliver
continue to use send_devise_notification. This is compatible with Devise's
documented model override that calls Action Mailer's deliver_later, including
for persisted placeholder resources. Action Mailer cannot enqueue an
unpersisted, resource-less invitee, so enable deliver_invitation_later when
create_resource_on_invitation is disabled. Enabling the setting or calling
Invitation#deliver_later gives the invitation job ownership of the queue
boundary. Inside that job, the gem calls the configured Devise mailer directly
with deliver_now, so a send_devise_notification override does not create a
second job. Other Devise notifications continue to use that override normally.
Applications without Active Job can leave deliver_invitation_later disabled
and continue to use synchronous delivery, or configure any compatible job class
that responds to perform_later.
For immediate resource-less background delivery, the encrypted acceptance token is a job argument. Treat the queue as sensitive storage and configure job logging so bearer tokens are not exposed. Mail options must also be serializable.
To check if a particular user is created by invitation, irrespective to state
of invitation one can use created_by_invite?
You can also set invited_by when using the invite! class method:
User.invite!({ email: 'new_user@example.com' }, current_user) # current_user will be set as invited_byYou can send an invitation to an existing user if your workflow creates them separately:
user = User.find(42)
user.invite!(current_user) # current user is optional to set the invited_by attributeWhenever possible, prefer the User.invite! class method described above. The
instance method creates another invitation and does not perform the
class method's checks for an existing registered account. Only call it from a
trusted, authorized workflow after verifying that the user is eligible to be
invited. In particular, do not expose it through an unauthenticated endpoint,
as doing so could allow an attacker to disrupt or take over an existing
account.
To find by invitation token use the find_by_invitation_token class method.
user = User.find_by_invitation_token(params[:invitation_token], true)To accept an invitation with a token use the accept_invitation! class
method. :invitation_token must be present in the parameters hash. You can
also include other attributes in the hash.
User.accept_invitation!(invitation_token: params[:invitation_token], password: 'ad97nwj3o2', name: 'John Doe')A callback event is fired before and after an invitation is created (User#invite!) or accepted (User#accept_invitation!). For example, in your resource model you can add:
# Note: callbacks should be placed after devise: :invitable is specified.
before_invitation_created :email_admins
after_invitation_accepted :email_invited_by
def email_admins
# ...
end
def email_invited_by
# ...
endThe callbacks support all options and arguments available to the standard callbacks provided by ActiveRecord.
A pair of scopes to find those users that have accepted, and those that have not accepted, invitations are defined:
User.invitation_accepted # => resources with at least one accepted invitation
User.invitation_not_accepted # => resources provisioned by a pending invitation
User.created_by_invite # => resources created through the invitation workflowSince the invitations controller takes care of all the creation/acceptation of
an invitation, in most cases you wouldn't call the invite! and
accept_invitation! methods directly. Instead, in your views, put a link to
new_user_invitation_path or new_invitation_path(:user) or even
/users/invitation/new to prepare and send an invitation (to a user in this
example).
After an invitation is created and sent, the inviter will be redirected to
after_invite_path_for(inviter, invitee), which is the same path as
signed_in_root_path by default.
After an invitation is accepted, the invitee will be redirected to
after_accept_path_for(resource), which is the same path as
signed_in_root_path by default. If you want to override the path, override
invitations controller and define after_accept_path_for method. This is
useful in the common case that a user is invited to a specific location in
your application. More on Devise's
README, "Controller filters and
helpers" section.
The invitation email includes a link to accept the invitation that looks like
this: /users/invitation/accept?invitation_token=abcd123. When clicked, the
invited must set a password in order to accept its invitation. Note that if
the invitation_token is not present or not valid, the invited is redirected
to invalid_token_path_for(resource_name), which by default is
after_sign_out_path_for(resource_name).
The controller sets the invitation's polymorphic invited_by association to
the current user. This keeps every inviter when the same address is invited
more than once.
InvitationsController uses authenticate_inviter! filter to restrict who can
send invitations. You can override this method in your
ApplicationController.
Default behavior requires authentication of the same resource as the invited
one. For example, if your model User is invitable, it will allow all
authenticated users to send invitations to other users.
You would have a User model which is configured as invitable and an Admin
model which is not. If you want to allow only admins to send invitations,
simply overwrite the authenticate_inviter! method as follow:
class ApplicationController < ActionController::Base
protected
def authenticate_inviter!
authenticate_admin!(force: true)
end
endAnd include DeviseInvitable::Inviter module into Admin model:
class Admin < ActiveRecord::Base
devise :database_authenticatable, :validatable
include DeviseInvitable::Inviter
endauthenticate_inviter! may enforce any application-specific condition, not
only authentication. For example, it can check roles, permissions,
relationships, tenant membership, or whether an application workflow has been
completed.
The filter protects only the invitation controller's new and create
actions. Calling User.invite! from an authorized application service,
controller, callback, or background job is unaffected.
To prevent invitations from being sent through the standard invitation form while still allowing controlled application workflows, override the filter in a custom invitations controller:
class Users::InvitationsController < Devise::InvitationsController
protected
def authenticate_inviter!
redirect_to root_path, alert: "Invitations cannot be sent from this page."
end
endConfigure Devise to use the controller:
devise_for :users, controllers: { invitations: "users/invitations" }The application can then create invitations from its authorized workflow:
User.invite!(email: email)When the standard form should remain available to some users, call super and
apply the additional policy:
class Users::InvitationsController < Devise::InvitationsController
protected
def authenticate_inviter!
super
return if performed?
return if current_user.allowed_to_invite_users?
redirect_to root_path, alert: "You are not allowed to send invitations."
end
endThe DeviseInvitable::Inviter module defines a sent_invitations association
containing every invitation sent by the resource:
current_user.sent_invitations
current_admin.sent_invitationsDeleting an inviter preserves its sent invitations and clears their
invited_by association.
DeviseInvitable uses flash messages with I18n with the flash keys
:send_instructions, :invitation_token_invalid and :updated. To customize
your app, you can modify the generated locale file:
en:
devise:
invitations:
send_instructions: 'An invitation email has been sent to %{email}.'
invitation_token_invalid: 'The invitation token provided is not valid!'
updated: 'Your password was set successfully. You are now signed in.'
updated_not_active: 'Your password was set successfully.'You can also create distinct messages based on the resource you've configured using the singular name given in routes:
en:
devise:
invitations:
user:
send_instructions: 'A new user invitation has been sent to %{email}.'
invitation_token_invalid: 'Your invitation token is not valid!'
updated: 'Welcome on board! You are now signed in.'
updated_not_active: 'Welcome on board! Sign in to continue.'The DeviseInvitable mailer uses the same pattern as Devise to create mail subject messages:
en:
devise:
mailer:
invitation_instructions:
subject: 'You got an invitation!'
user_subject: 'You got a user invitation!'Take a look at the generated locale file to check all available messages.
Check out wiki for translations.
If you are using sub schema in you application, you need to make sure that you
are prioritizing your sub schema scheme over Warden in Rack. For instance, if
you are using the Apartment gem go inside your config/application.rb file,
add the following lines:
module YourSite
class Application < Rails::Application
...
Rails.application.config.middleware.insert_before Warden::Manager, Apartment::Elevators::Subdomain
end
endDeviseInvitable supports ActiveRecord and Mongoid, like Devise.
It's possible to find additional information about DeviseInvitable on the Wiki:
https://github.com/scambra/devise_invitable/wiki
To run tests:
bundle install
bundle exec rake testCheck them all at:
https://github.com/scambra/devise_invitable/contributors
Special thanks to rymai for the Rails 3 support, his fork was a great help.
- Fork the project.
- Make your feature addition or bug fix.
- Add tests for it. This is important so I don't break it in a future version unintentionally.
- Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
- Send me a pull request. Bonus points for topic branches.
Copyright (c) 2019 Sergio Cambra. See LICENSE for details.