mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Beta Testing Round 3 Bug Fixes (#1357)
* Clean up env example files * Fix duplicate category creations * Fix duplicate tag and merchant creation * Add initial valuation to imported accounts * Add upgrade modal prompt * Don't hide content on billing page * Add temporary session for new customers * Lint fixes * Fix unused translations * Fix system tests
This commit is contained in:
parent
1d20de770f
commit
6baffe7539
43 changed files with 231 additions and 81 deletions
|
@ -1,3 +1,12 @@
|
||||||
|
# ================================ PLEASE READ ==========================================
|
||||||
|
# This file outlines all the possible environment variables supported by the Maybe app.
|
||||||
|
#
|
||||||
|
# This includes several features that are for our "hosted" version of Maybe, which most
|
||||||
|
# open-source contributors won't need.
|
||||||
|
#
|
||||||
|
# If you are developing locally, you should be referencing `.env.local.example` instead.
|
||||||
|
# =======================================================================================
|
||||||
|
|
||||||
# Custom port config
|
# Custom port config
|
||||||
# For users who have other applications listening at 3000, this allows them to set a value puma will listen to.
|
# For users who have other applications listening at 3000, this allows them to set a value puma will listen to.
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
5
.env.local.example
Normal file
5
.env.local.example
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# To enable / disable self-hosting features.
|
||||||
|
SELF_HOSTED=false
|
||||||
|
|
||||||
|
# Enable Synth market data (careful, this will use your API credits)
|
||||||
|
SYNTH_API_KEY=yourapikeyhere
|
8
.env.test
Normal file
8
.env.test
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
SELF_HOSTED=false
|
||||||
|
SYNTH_API_KEY=fookey
|
||||||
|
|
||||||
|
# Set to true if you want SimpleCov reports generated
|
||||||
|
COVERAGE=false
|
||||||
|
|
||||||
|
# Set to true to run test suite serially
|
||||||
|
DISABLE_PARALLELIZATION=false
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -10,8 +10,8 @@
|
||||||
# Ignore all environment files (except templates).
|
# Ignore all environment files (except templates).
|
||||||
/.env*
|
/.env*
|
||||||
!/.env*.erb
|
!/.env*.erb
|
||||||
!.env.example
|
!.env.test
|
||||||
!.env.test.example
|
!.env*.example
|
||||||
|
|
||||||
# Ignore all logfiles and tempfiles.
|
# Ignore all logfiles and tempfiles.
|
||||||
/log/*
|
/log/*
|
||||||
|
|
|
@ -49,7 +49,7 @@ After cloning the repo, the basic setup commands are:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd maybe
|
cd maybe
|
||||||
cp .env.example .env
|
cp .env.local.example .env.local
|
||||||
bin/setup
|
bin/setup
|
||||||
bin/dev
|
bin/dev
|
||||||
|
|
||||||
|
|
BIN
app/assets/images/maybe-plus-background.png
Normal file
BIN
app/assets/images/maybe-plus-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
app/assets/images/maybe-plus-logo.png
Normal file
BIN
app/assets/images/maybe-plus-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
6
app/assets/images/stripe-logo.svg
Normal file
6
app/assets/images/stripe-logo.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Frame 1321315963">
|
||||||
|
<rect width="20" height="20" rx="10" fill="#635BFF"/>
|
||||||
|
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M9.35663 7.69056C9.35663 7.20077 9.75747 7.01238 10.4214 7.01238C11.3734 7.01238 12.5759 7.30124 13.5279 7.81615V4.86482C12.4882 4.45037 11.461 4.28711 10.4214 4.28711C7.87854 4.28711 6.1875 5.61835 6.1875 7.84127C6.1875 11.3075 10.9475 10.7549 10.9475 12.2494C10.9475 12.8271 10.4464 13.0155 9.74495 13.0155C8.70527 13.0155 7.37749 12.5885 6.32529 12.0108V14.9998C7.49023 15.5022 8.66769 15.7157 9.74495 15.7157C12.3504 15.7157 14.1416 14.4221 14.1416 12.1741C14.1291 8.43154 9.35663 9.09716 9.35663 7.69056Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 775 B |
|
@ -2,7 +2,22 @@ class ApplicationController < ActionController::Base
|
||||||
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
|
||||||
|
helper_method :require_upgrade?, :subscription_pending?
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def require_upgrade?
|
||||||
|
return false if self_hosted?
|
||||||
|
return false unless Current.session
|
||||||
|
return false if Current.family.subscribed?
|
||||||
|
return false if subscription_pending? || request.path == settings_billing_path
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscription_pending?
|
||||||
|
subscribed_at = Current.session.subscribed_at
|
||||||
|
subscribed_at.present? && subscribed_at <= Time.current && subscribed_at > 1.hour.ago
|
||||||
|
end
|
||||||
|
|
||||||
def with_sidebar
|
def with_sidebar
|
||||||
return "turbo_rails/frame" if turbo_frame_request?
|
return "turbo_rails/frame" if turbo_frame_request?
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class CategoriesController < ApplicationController
|
class CategoriesController < ApplicationController
|
||||||
layout :with_sidebar
|
layout :with_sidebar
|
||||||
|
|
||||||
before_action :set_category, only: %i[edit update]
|
before_action :set_category, only: %i[edit update destroy]
|
||||||
before_action :set_transaction, only: :create
|
before_action :set_transaction, only: :create
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -13,12 +13,14 @@ class CategoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
Category.transaction do
|
@category = Current.family.categories.new(category_params)
|
||||||
category = Current.family.categories.create!(category_params)
|
|
||||||
@transaction.update!(category_id: category.id) if @transaction
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_back_or_to transactions_path, notice: t(".success")
|
if @category.save
|
||||||
|
@transaction.update(category_id: @category.id) if @transaction
|
||||||
|
redirect_back_or_to transactions_path, notice: t(".success")
|
||||||
|
else
|
||||||
|
redirect_back_or_to transactions_path, alert: t(".failure", error: @category.errors.full_messages.to_sentence)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
@ -30,6 +32,12 @@ class CategoriesController < ApplicationController
|
||||||
redirect_back_or_to transactions_path, notice: t(".success")
|
redirect_back_or_to transactions_path, notice: t(".success")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@category.destroy
|
||||||
|
|
||||||
|
redirect_back_or_to categories_path, notice: t(".success")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def set_category
|
def set_category
|
||||||
@category = Current.family.categories.find(params[:id])
|
@category = Current.family.categories.find(params[:id])
|
||||||
|
|
|
@ -12,8 +12,13 @@ class MerchantsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
Current.family.merchants.create!(merchant_params)
|
@merchant = Current.family.merchants.new(merchant_params)
|
||||||
redirect_to merchants_path, notice: t(".success")
|
|
||||||
|
if @merchant.save
|
||||||
|
redirect_to merchants_path, notice: t(".success")
|
||||||
|
else
|
||||||
|
redirect_to merchants_path, alert: t(".error", error: @merchant.errors.full_messages.to_sentence)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
class SubscriptionsController < ApplicationController
|
class SubscriptionsController < ApplicationController
|
||||||
def new
|
def new
|
||||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
|
||||||
|
|
||||||
if Current.family.stripe_customer_id.blank?
|
if Current.family.stripe_customer_id.blank?
|
||||||
customer = client.v1.customers.create(
|
customer = stripe_client.v1.customers.create(
|
||||||
email: Current.family.primary_user.email,
|
email: Current.family.primary_user.email,
|
||||||
metadata: { family_id: Current.family.id }
|
metadata: { family_id: Current.family.id }
|
||||||
)
|
)
|
||||||
Current.family.update(stripe_customer_id: customer.id)
|
Current.family.update(stripe_customer_id: customer.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
session = client.v1.checkout.sessions.create({
|
session = stripe_client.v1.checkout.sessions.create({
|
||||||
customer: Current.family.stripe_customer_id,
|
customer: Current.family.stripe_customer_id,
|
||||||
line_items: [ {
|
line_items: [ {
|
||||||
price: ENV["STRIPE_PLAN_ID"],
|
price: ENV["STRIPE_PLAN_ID"],
|
||||||
|
@ -18,7 +16,7 @@ class SubscriptionsController < ApplicationController
|
||||||
} ],
|
} ],
|
||||||
mode: "subscription",
|
mode: "subscription",
|
||||||
allow_promotion_codes: true,
|
allow_promotion_codes: true,
|
||||||
success_url: settings_billing_url,
|
success_url: success_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||||
cancel_url: settings_billing_url
|
cancel_url: settings_billing_url
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -26,12 +24,24 @@ class SubscriptionsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
client = Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
portal_session = stripe_client.v1.billing_portal.sessions.create(
|
||||||
|
|
||||||
portal_session = client.v1.billing_portal.sessions.create(
|
|
||||||
customer: Current.family.stripe_customer_id,
|
customer: Current.family.stripe_customer_id,
|
||||||
return_url: settings_billing_url
|
return_url: settings_billing_url
|
||||||
)
|
)
|
||||||
|
|
||||||
redirect_to portal_session.url, allow_other_host: true, status: :see_other
|
redirect_to portal_session.url, allow_other_host: true, status: :see_other
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def success
|
||||||
|
checkout_session = stripe_client.v1.checkout.sessions.retrieve(params[:session_id])
|
||||||
|
Current.session.update(subscribed_at: Time.at(checkout_session.created))
|
||||||
|
redirect_to root_path, notice: "You have successfully subscribed to Maybe+."
|
||||||
|
rescue Stripe::InvalidRequestError
|
||||||
|
redirect_to settings_billing_path, alert: "Something went wrong processing your subscription. Please contact us to get this fixed."
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def stripe_client
|
||||||
|
@stripe_client ||= Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
layout :with_sidebar
|
layout :with_sidebar
|
||||||
|
|
||||||
before_action :set_tag, only: %i[edit update]
|
before_action :set_tag, only: %i[edit update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@tags = Current.family.tags.alphabetically
|
@tags = Current.family.tags.alphabetically
|
||||||
|
@ -12,8 +12,13 @@ class TagsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
Current.family.tags.create!(tag_params)
|
@tag = Current.family.tags.new(tag_params)
|
||||||
redirect_to tags_path, notice: t(".created")
|
|
||||||
|
if @tag.save
|
||||||
|
redirect_to tags_path, notice: t(".created")
|
||||||
|
else
|
||||||
|
redirect_to tags_path, alert: t(".error", error: @tag.errors.full_messages.to_sentence)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
@ -24,6 +29,11 @@ class TagsController < ApplicationController
|
||||||
redirect_to tags_path, notice: t(".updated")
|
redirect_to tags_path, notice: t(".updated")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@tag.destroy!
|
||||||
|
redirect_to tags_path, notice: t(".deleted")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_tag
|
def set_tag
|
||||||
|
|
|
@ -5,10 +5,10 @@ module SettingsHelper
|
||||||
{ name: I18n.t("settings.nav.self_hosting_label"), path: :settings_hosting_path, condition: :self_hosted? },
|
{ name: I18n.t("settings.nav.self_hosting_label"), path: :settings_hosting_path, condition: :self_hosted? },
|
||||||
{ name: I18n.t("settings.nav.billing_label"), path: :settings_billing_path },
|
{ name: I18n.t("settings.nav.billing_label"), path: :settings_billing_path },
|
||||||
{ name: I18n.t("settings.nav.accounts_label"), path: :accounts_path },
|
{ name: I18n.t("settings.nav.accounts_label"), path: :accounts_path },
|
||||||
|
{ name: I18n.t("settings.nav.imports_label"), path: :imports_path },
|
||||||
{ name: I18n.t("settings.nav.tags_label"), path: :tags_path },
|
{ name: I18n.t("settings.nav.tags_label"), path: :tags_path },
|
||||||
{ name: I18n.t("settings.nav.categories_label"), path: :categories_path },
|
{ name: I18n.t("settings.nav.categories_label"), path: :categories_path },
|
||||||
{ name: I18n.t("settings.nav.merchants_label"), path: :merchants_path },
|
{ name: I18n.t("settings.nav.merchants_label"), path: :merchants_path },
|
||||||
{ name: I18n.t("settings.nav.imports_label"), path: :imports_path },
|
|
||||||
{ name: I18n.t("settings.nav.whats_new_label"), path: :changelog_path },
|
{ name: I18n.t("settings.nav.whats_new_label"), path: :changelog_path },
|
||||||
{ name: I18n.t("settings.nav.feedback_label"), path: :feedback_path }
|
{ name: I18n.t("settings.nav.feedback_label"), path: :feedback_path }
|
||||||
]
|
]
|
||||||
|
|
|
@ -49,7 +49,12 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit(value = nil, options = {})
|
def submit(value = nil, options = {})
|
||||||
merged_options = { class: "btn btn--primary w-full" }.merge(options)
|
default_options = {
|
||||||
|
data: { turbo_submits_with: "Submitting..." },
|
||||||
|
class: "btn btn--primary w-full"
|
||||||
|
}
|
||||||
|
|
||||||
|
merged_options = default_options.merge(options)
|
||||||
value, options = nil, value if value.is_a?(Hash)
|
value, options = nil, value if value.is_a?(Hash)
|
||||||
super(value, merged_options)
|
super(value, merged_options)
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,14 @@ class AccountImport < Import
|
||||||
)
|
)
|
||||||
|
|
||||||
account.save!
|
account.save!
|
||||||
|
|
||||||
|
account.entries.create!(
|
||||||
|
amount: row.amount,
|
||||||
|
currency: row.currency,
|
||||||
|
date: Date.current,
|
||||||
|
name: "Imported account value",
|
||||||
|
entryable: Account::Valuation.new
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ class Category < ApplicationRecord
|
||||||
belongs_to :family
|
belongs_to :family
|
||||||
|
|
||||||
validates :name, :color, :family, presence: true
|
validates :name, :color, :family, presence: true
|
||||||
|
validates :name, uniqueness: { scope: :family_id }
|
||||||
|
|
||||||
before_update :clear_internal_category, if: :name_changed?
|
before_update :clear_internal_category, if: :name_changed?
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,8 @@ class Demo::Generator
|
||||||
first_name: "Demo",
|
first_name: "Demo",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
role: "admin",
|
role: "admin",
|
||||||
password: "password"
|
password: "password",
|
||||||
|
onboarded_at: Time.current
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_tags!
|
def create_tags!
|
||||||
|
|
|
@ -132,7 +132,7 @@ class Family < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribed?
|
def subscribed?
|
||||||
stripe_subscription_status.present? && stripe_subscription_status == "active"
|
stripe_subscription_status == "active"
|
||||||
end
|
end
|
||||||
|
|
||||||
def primary_user
|
def primary_user
|
||||||
|
|
|
@ -3,6 +3,7 @@ class Merchant < ApplicationRecord
|
||||||
belongs_to :family
|
belongs_to :family
|
||||||
|
|
||||||
validates :name, :color, :family, presence: true
|
validates :name, :color, :family, presence: true
|
||||||
|
validates :name, uniqueness: { scope: :family }
|
||||||
|
|
||||||
scope :alphabetically, -> { order(:name) }
|
scope :alphabetically, -> { order(:name) }
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,15 @@
|
||||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||||
<%= contextual_menu_modal_action_item t(".edit"), edit_category_path(category) %>
|
<%= contextual_menu_modal_action_item t(".edit"), edit_category_path(category) %>
|
||||||
|
|
||||||
<%= link_to new_category_deletion_path(category),
|
<% if category.transactions.any? %>
|
||||||
|
<%= link_to new_category_deletion_path(category),
|
||||||
class: "flex items-center w-full rounded-lg text-red-600 hover:bg-red-50 py-2 px-3 gap-2",
|
class: "flex items-center w-full rounded-lg text-red-600 hover:bg-red-50 py-2 px-3 gap-2",
|
||||||
data: { turbo_frame: :modal } do %>
|
data: { turbo_frame: :modal } do %>
|
||||||
<%= lucide_icon "trash-2", class: "shrink-0 w-5 h-5" %>
|
<%= lucide_icon "trash-2", class: "shrink-0 w-5 h-5" %>
|
||||||
<span class="text-sm"><%= t(".delete") %></span>
|
<span class="text-sm"><%= t(".delete") %></span>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= contextual_menu_destructive_item t(".delete"), category_path(category), turbo_confirm: nil %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div data-controller="color-avatar">
|
<div data-controller="color-avatar">
|
||||||
<%= styled_form_with model: category, class: "space-y-4", data: { turbo: false } do |f| %>
|
<%= styled_form_with model: category, class: "space-y-4", data: { turbo_frame: :_top } do |f| %>
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<div class="w-fit m-auto">
|
<div class="w-fit m-auto">
|
||||||
<%= render partial: "shared/color_avatar", locals: { name: category.name, color: category.color } %>
|
<%= render partial: "shared/color_avatar", locals: { name: category.name, color: category.color } %>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex items-center border border-gray-200 rounded-lg">
|
<div class="relative flex items-center border border-gray-200 rounded-lg">
|
||||||
<%= f.text_field :name, placeholder: t(".placeholder"), class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-3 w-full border-none rounded-lg", required: true, data: { color_avatar_target: "name" } %>
|
<%= f.text_field :name, placeholder: t(".placeholder"), required: true, autofocus: true, data: { color_avatar_target: "name" } %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,12 @@
|
||||||
<%= render "layouts/sidebar" %>
|
<%= render "layouts/sidebar" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<main class="grow px-20 py-6 h-full overflow-y-auto">
|
|
||||||
|
<main class="grow px-20 py-6 h-full relative <%= require_upgrade? ? "overflow-hidden" : "overflow-y-auto" %>">
|
||||||
|
<% if require_upgrade? %>
|
||||||
|
<%= render "shared/subscribe_modal" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div data-controller="color-avatar">
|
<div data-controller="color-avatar">
|
||||||
<%= styled_form_with model: @merchant, class: "space-y-4", data: { turbo: false } do |f| %>
|
<%= styled_form_with model: @merchant, class: "space-y-4", data: { turbo_frame: :_top } do |f| %>
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<div class="w-fit m-auto">
|
<div class="w-fit m-auto">
|
||||||
<%= render partial: "shared/color_avatar", locals: { name: @merchant.name, color: @merchant.color } %>
|
<%= render partial: "shared/color_avatar", locals: { name: @merchant.name, color: @merchant.color } %>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex items-center border border-gray-200 rounded-lg">
|
<div class="relative flex items-center border border-gray-200 rounded-lg">
|
||||||
<%= f.text_field :name, placeholder: t(".name_placeholder"), class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-3 w-full border-none rounded-lg", required: true, data: { color_avatar_target: "name" } %>
|
<%= f.text_field :name, placeholder: t(".name_placeholder"), autofocus: true, required: true, data: { color_avatar_target: "name" } %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
<%= contextual_menu_destructive_item t(".delete"),
|
<%= contextual_menu_destructive_item t(".delete"),
|
||||||
merchant_path(merchant),
|
merchant_path(merchant),
|
||||||
turbo_frame: "_top",
|
turbo_frame: "_top",
|
||||||
turbo_confirm: {
|
turbo_confirm: merchant.transactions.any? ? {
|
||||||
title: t(".confirm_title"),
|
title: t(".confirm_title"),
|
||||||
body: t(".confirm_body"),
|
body: t(".confirm_body"),
|
||||||
accept: t(".confirm_accept")
|
accept: t(".confirm_accept")
|
||||||
} %>
|
} : nil %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
|
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-1 mb-2">
|
<div class="p-1 bg-alpha-black-25 mb-2 rounded-lg">
|
||||||
<div class="bg-white p-4 rounded-lg flex gap-8" style="box-shadow: 0px 0px 0px 1px rgba(11, 11, 11, 0.05), 0px 1px 2px 0px rgba(11, 11, 11, 0.05);">
|
<div class="bg-white p-4 rounded-lg flex gap-8 shadow-xs">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= tag.p t(".example"), class: "text-gray-500 text-sm" %>
|
<%= tag.p t(".example"), class: "text-gray-500 text-sm" %>
|
||||||
<%= tag.p "$2,323.25", class: "text-gray-900 font-medium text-2xl" %>
|
<%= tag.p "$2,323.25", class: "text-gray-900 font-medium text-2xl" %>
|
||||||
|
|
|
@ -24,9 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<% if !Current.family.subscribed? && !self_hosted? %>
|
<% if @accounts.empty? %>
|
||||||
<%= render "shared/subscribe_prompt" %>
|
|
||||||
<% elsif @accounts.empty? %>
|
|
||||||
<%= render "shared/no_account_empty_state" %>
|
<%= render "shared/no_account_empty_state" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<section class="flex gap-4">
|
<section class="flex gap-4">
|
||||||
|
|
|
@ -5,11 +5,42 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||||
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
|
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
|
||||||
<% if @user.family.stripe_plan_id.blank? %>
|
<div class="space-y-4">
|
||||||
<%= link_to t(".subscribe_button"), new_subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
<div class="p-3 shadow-xs bg-white border border-alpha-black-200 rounded-lg flex justify-between items-center">
|
||||||
<% else %>
|
<div class="flex items-center gap-3">
|
||||||
<%= link_to t(".manage_subscription_button"), subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
|
||||||
<% end %>
|
<%= lucide_icon "gem", class: "w-5 h-5 text-gray-500" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm space-y-1">
|
||||||
|
<% if @user.family.subscribed? || subscription_pending? %>
|
||||||
|
<p class="text-gray-900">You are currently subscribed to <span class="font-medium">Maybe+</span></p>
|
||||||
|
<p class="text-gray-500">Manage your billing settings here.</p>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-gray-900">You are currently <span class="font-medium">not subscribed</span></p>
|
||||||
|
<p class="text-gray-500">Once you subscribe to Maybe+, you’ll see your billing settings here.</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if @user.family.subscribed? || subscription_pending? %>
|
||||||
|
<%= link_to subscription_path, class: "btn btn--secondary flex items-center gap-1" do %>
|
||||||
|
<span>Manage</span>
|
||||||
|
<%= lucide_icon "external-link", class: "w-5 h-5 shrink-0 text-gray-500" %>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= link_to new_subscription_path, class: "btn btn--secondary flex items-center gap-1" do %>
|
||||||
|
<span>Subscribe</span>
|
||||||
|
<%= lucide_icon "external-link", class: "w-5 h-5 shrink-0 text-gray-500" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= image_tag "stripe-logo.svg", class: "w-5 h-5 shrink-0" %>
|
||||||
|
<p class="text-gray-500 text-sm">Managed via Stripe</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= settings_nav_footer %>
|
<%= settings_nav_footer %>
|
||||||
|
|
25
app/views/shared/_subscribe_modal.html.erb
Normal file
25
app/views/shared/_subscribe_modal.html.erb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<div data-controller="modal" data-modal-open-value="true" class="absolute inset-0 z-50 flex items-center justify-center bg-white/90" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||||
|
<div class="w-[400px] rounded-xl relative overflow-hidden">
|
||||||
|
<div class="bg-white shadow-xl border border-gray-200 rounded-xl relative z-10">
|
||||||
|
<div class="rounded-xl" style="background-image: url('<%= asset_path("maybe-plus-background.png") %>'); background-size: cover; background-position: center;">
|
||||||
|
<div class="text-center rounded-xl" style="background-image: linear-gradient(to bottom, rgba(197,161,119,0.15) 0%, rgba(255,255,255,0.8) 30%, white 40%);">
|
||||||
|
<div class="p-4 rounded-xl">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<%= image_tag "maybe-plus-logo.png", class: "w-16" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="font-medium text-gray-900 mb-2">Join Maybe+</h2>
|
||||||
|
|
||||||
|
<div class="text-gray-500 text-sm space-y-4 mb-5">
|
||||||
|
<p>Nobody likes paywalls, but we need feedback from users willing to pay for Maybe. </p>
|
||||||
|
|
||||||
|
<p>To continue using the app, please subscribe. In this early beta testing phase, we require that you upgrade within 1 hour to claim your spot.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= link_to "Upgrade to Maybe+", new_subscription_path, class: "btn btn--primary text-center w-full block" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,16 +0,0 @@
|
||||||
<div class="flex justify-center items-center h-[800px]">
|
|
||||||
<div class="text-center flex flex-col gap-4 items-center max-w-[300px]">
|
|
||||||
<%= lucide_icon "circle-fading-arrow-up", class: "w-8 h-8 text-green-500" %>
|
|
||||||
|
|
||||||
<div class="space-y-1 text-sm">
|
|
||||||
<p class="text-gray-900 font-medium"><%= t(".title") %></p>
|
|
||||||
<p class="text-gray-500"><%= t(".subtitle") %></p>
|
|
||||||
<p class="text-gray-400 text-xs"><%= t(".guarantee") %></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= link_to new_subscription_path, class: "btn btn--primary flex items-center gap-1" do %>
|
|
||||||
<%= lucide_icon("credit-card", class: "w-5 h-5") %>
|
|
||||||
<span><%= t(".subscribe") %></span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div data-controller="color-avatar">
|
<div data-controller="color-avatar">
|
||||||
<%= styled_form_with model: tag, class: "space-y-4", data: { turbo: false } do |f| %>
|
<%= styled_form_with model: tag, class: "space-y-4", data: { turbo_frame: :_top } do |f| %>
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<div class="w-fit m-auto">
|
<div class="w-fit m-auto">
|
||||||
<%= render partial: "shared/color_avatar", locals: { name: tag.name, color: tag.color } %>
|
<%= render partial: "shared/color_avatar", locals: { name: tag.name, color: tag.color } %>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex items-center border border-gray-200 rounded-lg">
|
<div class="relative flex items-center border border-gray-200 rounded-lg">
|
||||||
<%= f.text_field :name, placeholder: t(".placeholder"), class: "text-sm font-normal placeholder:text-gray-500 h-10 relative pl-3 w-full border-none rounded-lg", required: true, data: { color_avatar_target: "name" } %>
|
<%= f.text_field :name, placeholder: t(".placeholder"), autofocus: true, required: true, data: { color_avatar_target: "name" } %>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,15 @@
|
||||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||||
<%= contextual_menu_modal_action_item t(".edit"), edit_tag_path(tag) %>
|
<%= contextual_menu_modal_action_item t(".edit"), edit_tag_path(tag) %>
|
||||||
|
|
||||||
<%= link_to new_tag_deletion_path(tag),
|
<% if tag.transactions.any? %>
|
||||||
|
<%= link_to new_tag_deletion_path(tag),
|
||||||
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
class: "block w-full py-2 px-3 space-x-2 text-red-600 hover:bg-red-50 flex items-center rounded-lg",
|
||||||
data: { turbo_frame: :modal } do %>
|
data: { turbo_frame: :modal } do %>
|
||||||
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
|
<%= lucide_icon "trash-2", class: "w-5 h-5" %>
|
||||||
<span><%= t(".delete") %></span>
|
<span><%= t(".delete") %></span>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= contextual_menu_destructive_item t(".delete"), tag_path(tag), turbo_confirm: nil %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -5,7 +5,10 @@ en:
|
||||||
delete: Delete category
|
delete: Delete category
|
||||||
edit: Edit category
|
edit: Edit category
|
||||||
create:
|
create:
|
||||||
|
failure: 'Failed to create category: %{error}'
|
||||||
success: New transaction category created successfully
|
success: New transaction category created successfully
|
||||||
|
destroy:
|
||||||
|
success: Category deleted successfully
|
||||||
edit:
|
edit:
|
||||||
edit: Edit category
|
edit: Edit category
|
||||||
form:
|
form:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
en:
|
en:
|
||||||
merchants:
|
merchants:
|
||||||
create:
|
create:
|
||||||
|
error: 'Error creating merchant: %{error}'
|
||||||
success: New merchant created successfully
|
success: New merchant created successfully
|
||||||
destroy:
|
destroy:
|
||||||
success: Merchant deleted successfully
|
success: Merchant deleted successfully
|
||||||
|
|
|
@ -3,9 +3,7 @@ en:
|
||||||
settings:
|
settings:
|
||||||
billings:
|
billings:
|
||||||
show:
|
show:
|
||||||
manage_subscription_button: Manage subscription
|
|
||||||
page_title: Billing
|
page_title: Billing
|
||||||
subscribe_button: Subscribe
|
|
||||||
subscription_subtitle: Manage your subscription and billing details
|
subscription_subtitle: Manage your subscription and billing details
|
||||||
subscription_title: Manage subscription
|
subscription_title: Manage subscription
|
||||||
nav:
|
nav:
|
||||||
|
|
|
@ -13,12 +13,6 @@ en:
|
||||||
no_account_subtitle: Since no accounts have been added, there's no data to display.
|
no_account_subtitle: Since no accounts have been added, there's no data to display.
|
||||||
Add your first accounts to start viewing dashboard data.
|
Add your first accounts to start viewing dashboard data.
|
||||||
no_account_title: No accounts yet
|
no_account_title: No accounts yet
|
||||||
subscribe_prompt:
|
|
||||||
guarantee: We're reasonable people here. If you're not happy or something doesn't
|
|
||||||
work, we'll gladly refund you.
|
|
||||||
subscribe: Upgrade your account
|
|
||||||
subtitle: To continue using Maybe, please subscribe!
|
|
||||||
title: Upgrade
|
|
||||||
upgrade_notification:
|
upgrade_notification:
|
||||||
app_upgraded: The app has been upgraded to %{version}.
|
app_upgraded: The app has been upgraded to %{version}.
|
||||||
dismiss: Dismiss
|
dismiss: Dismiss
|
||||||
|
|
|
@ -3,6 +3,9 @@ en:
|
||||||
tags:
|
tags:
|
||||||
create:
|
create:
|
||||||
created: Tag created
|
created: Tag created
|
||||||
|
error: 'Error creating tag: %{error}'
|
||||||
|
destroy:
|
||||||
|
deleted: Tag deleted
|
||||||
edit:
|
edit:
|
||||||
edit: Edit tag
|
edit: Edit tag
|
||||||
form:
|
form:
|
||||||
|
|
|
@ -26,9 +26,11 @@ Rails.application.routes.draw do
|
||||||
resource :billing, only: :show
|
resource :billing, only: :show
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :subscription, only: %i[new show]
|
resource :subscription, only: %i[new show] do
|
||||||
|
get :success, on: :collection
|
||||||
|
end
|
||||||
|
|
||||||
resources :tags, except: %i[show destroy] do
|
resources :tags, except: :show do
|
||||||
resources :deletions, only: %i[new create], module: :tag
|
resources :deletions, only: %i[new create], module: :tag
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddSubscriptionTimestampToSession < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :sessions, :subscribed_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
3
db/schema.rb
generated
3
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2024_10_22_221544) do
|
ActiveRecord::Schema[7.2].define(version: 2024_10_24_142537) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -496,6 +496,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_22_221544) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.uuid "active_impersonator_session_id"
|
t.uuid "active_impersonator_session_id"
|
||||||
|
t.datetime "subscribed_at"
|
||||||
t.index ["active_impersonator_session_id"], name: "index_sessions_on_active_impersonator_session_id"
|
t.index ["active_impersonator_session_id"], name: "index_sessions_on_active_impersonator_session_id"
|
||||||
t.index ["user_id"], name: "index_sessions_on_user_id"
|
t.index ["user_id"], name: "index_sessions_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
2
test/fixtures/families.yml
vendored
2
test/fixtures/families.yml
vendored
|
@ -1,6 +1,8 @@
|
||||||
empty:
|
empty:
|
||||||
name: Family
|
name: Family
|
||||||
|
stripe_subscription_status: active
|
||||||
|
|
||||||
dylan_family:
|
dylan_family:
|
||||||
name: The Dylan Family
|
name: The Dylan Family
|
||||||
|
stripe_subscription_status: active
|
||||||
|
|
||||||
|
|
2
test/fixtures/files/imports/accounts.csv
vendored
2
test/fixtures/files/imports/accounts.csv
vendored
|
@ -1,5 +1,5 @@
|
||||||
type,name,amount,currency
|
type,name,amount,currency
|
||||||
Checking,Main Checking Account,5000.00,USD
|
Checking,Main Checking Account,5000.00,USD
|
||||||
Savings,Emergency Fund,10000.00,USD
|
Savings,Emergency Fund,10000.00,USD
|
||||||
Credit Card,Rewards Credit Card,-1500.00,USD
|
Credit Card,Rewards Credit Card,1500.00,USD
|
||||||
Investment,Retirement Portfolio,75000.00,USD
|
Investment,Retirement Portfolio,75000.00,USD
|
||||||
|
|
|
|
@ -7,8 +7,6 @@ end
|
||||||
|
|
||||||
require_relative "../config/environment"
|
require_relative "../config/environment"
|
||||||
|
|
||||||
ENV["SELF_HOSTED"] = "false"
|
|
||||||
ENV["UPGRADES_ENABLED"] = "false"
|
|
||||||
ENV["RAILS_ENV"] ||= "test"
|
ENV["RAILS_ENV"] ||= "test"
|
||||||
|
|
||||||
# Fixes Segfaults on M1 Macs when running tests in parallel (temporary workaround)
|
# Fixes Segfaults on M1 Macs when running tests in parallel (temporary workaround)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue