mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Add trial mode
This commit is contained in:
parent
f6b7652a01
commit
71488c9fb1
34 changed files with 848 additions and 371 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
|
||||
- X-Dawarich-Response and X-Dawarich-Version headers are now returned for all API responses.
|
||||
|
||||
|
||||
# [0.30.8] - 2025-08-01
|
||||
|
||||
## Fixed
|
||||
|
|
|
|||
3
Gemfile
3
Gemfile
|
|
@ -5,6 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
|||
|
||||
ruby File.read('.ruby-version').strip
|
||||
|
||||
gem 'activerecord-postgis-adapter'
|
||||
# https://meta.discourse.org/t/cant-rebuild-due-to-aws-sdk-gem-bump-and-new-aws-data-integrity-protections/354217/40
|
||||
gem 'aws-sdk-s3', '~> 1.177.0', require: false
|
||||
gem 'aws-sdk-core', '~> 3.215.1', require: false
|
||||
|
|
@ -24,7 +25,7 @@ gem 'oj'
|
|||
gem 'parallel'
|
||||
gem 'pg'
|
||||
gem 'prometheus_exporter'
|
||||
gem 'activerecord-postgis-adapter'
|
||||
gem 'rqrcode', '~> 3.0'
|
||||
gem 'puma'
|
||||
gem 'pundit'
|
||||
gem 'rails', '~> 8.0'
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ GEM
|
|||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
chartkick (5.2.0)
|
||||
chunky_png (1.4.0)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
|
|
@ -365,6 +366,10 @@ GEM
|
|||
rgeo-geojson (2.2.0)
|
||||
multi_json (~> 1.15)
|
||||
rgeo (>= 1.0.0)
|
||||
rqrcode (3.1.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rspec-core (3.13.3)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.4)
|
||||
|
|
@ -553,6 +558,7 @@ DEPENDENCIES
|
|||
rgeo
|
||||
rgeo-activerecord
|
||||
rgeo-geojson
|
||||
rqrcode (~> 3.0)
|
||||
rspec-rails
|
||||
rswag-api
|
||||
rswag-specs
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ module ApplicationHelper
|
|||
|
||||
grouped_by_country[country] ||= []
|
||||
|
||||
if toponym['cities'].present?
|
||||
toponym['cities'].each do |city_data|
|
||||
city = city_data['city']
|
||||
grouped_by_country[country] << city if city.present?
|
||||
end
|
||||
next unless toponym['cities'].present?
|
||||
|
||||
toponym['cities'].each do |city_data|
|
||||
city = city_data['city']
|
||||
grouped_by_country[country] << city if city.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -172,4 +172,21 @@ module ApplicationHelper
|
|||
data: { tip: "Expires on #{active_until.iso8601}" }
|
||||
)
|
||||
end
|
||||
|
||||
def onboarding_modal_showable?(user)
|
||||
user.trial_state?
|
||||
end
|
||||
|
||||
def trial_button_class(user)
|
||||
case (user.active_until.to_date - Time.current.to_date).to_i
|
||||
when 5..8
|
||||
'btn-info'
|
||||
when 2...5
|
||||
'btn-warning'
|
||||
when 0...2
|
||||
'btn-error'
|
||||
else
|
||||
'btn-success'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
17
app/helpers/user_helper.rb
Normal file
17
app/helpers/user_helper.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module UserHelper
|
||||
def api_key_qr_code(user)
|
||||
qrcode = RQRCode::QRCode.new(user.api_key)
|
||||
svg = qrcode.as_svg(
|
||||
color: "000",
|
||||
fill: "fff",
|
||||
shape_rendering: "crispEdges",
|
||||
module_size: 11,
|
||||
standalone: true,
|
||||
use_path: true,
|
||||
offset: 5
|
||||
)
|
||||
svg.html_safe
|
||||
end
|
||||
end
|
||||
13
app/jobs/users/mailer_sending_job.rb
Normal file
13
app/jobs/users/mailer_sending_job.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::MailerSendingJob < ApplicationJob
|
||||
queue_as :mailers
|
||||
|
||||
def perform(user_id, email_type, **options)
|
||||
user = User.find(user_id)
|
||||
|
||||
params = { user: user }.merge(options)
|
||||
|
||||
UsersMailer.with(params).public_send(email_type).deliver_later
|
||||
end
|
||||
end
|
||||
22
app/jobs/users/trial_webhook_job.rb
Normal file
22
app/jobs/users/trial_webhook_job.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::TrialWebhookJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user_id)
|
||||
user = User.find(user_id)
|
||||
|
||||
token = Subscription::EncodeJwtToken.new(
|
||||
{ user_id: user.id, email: user.email, action: 'create_user' },
|
||||
ENV['JWT_SECRET_KEY']
|
||||
).call
|
||||
|
||||
request_url = "#{ENV['MANAGER_URL']}/api/v1/users"
|
||||
headers = {
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
}
|
||||
|
||||
HTTParty.post(request_url, headers: headers, body: { token: token }.to_json)
|
||||
end
|
||||
end
|
||||
27
app/mailers/users_mailer.rb
Normal file
27
app/mailers/users_mailer.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UsersMailer < ApplicationMailer
|
||||
def welcome
|
||||
@user = params[:user]
|
||||
|
||||
mail(to: @user.email, subject: 'Welcome to Dawarich')
|
||||
end
|
||||
|
||||
def explore_features
|
||||
@user = params[:user]
|
||||
|
||||
mail(to: @user.email, subject: 'Explore Dawarich features')
|
||||
end
|
||||
|
||||
def trial_expires_soon
|
||||
@user = params[:user]
|
||||
|
||||
mail(to: @user.email, subject: 'Your Dawarich trial expires in 2 days')
|
||||
end
|
||||
|
||||
def trial_expired
|
||||
@user = params[:user]
|
||||
|
||||
mail(to: @user.email, subject: 'Your Dawarich trial expired')
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class User < ApplicationRecord
|
||||
class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable, :trackable
|
||||
|
||||
|
|
@ -18,6 +18,8 @@ class User < ApplicationRecord
|
|||
|
||||
after_create :create_api_key
|
||||
after_commit :activate, on: :create, if: -> { DawarichSettings.self_hosted? }
|
||||
after_commit :start_trial, on: :create, if: -> { !DawarichSettings.self_hosted? }
|
||||
after_commit :schedule_welcome_emails, on: :create, if: -> { !DawarichSettings.self_hosted? }
|
||||
before_save :sanitize_input
|
||||
|
||||
validates :email, presence: true
|
||||
|
|
@ -26,7 +28,7 @@ class User < ApplicationRecord
|
|||
|
||||
attribute :admin, :boolean, default: false
|
||||
|
||||
enum :status, { inactive: 0, active: 1 }
|
||||
enum :status, { inactive: 0, active: 1, trial: 3 }
|
||||
|
||||
def safe_settings
|
||||
Users::SafeSettings.new(settings)
|
||||
|
|
@ -115,6 +117,10 @@ class User < ApplicationRecord
|
|||
Users::ExportDataJob.perform_later(id)
|
||||
end
|
||||
|
||||
def trial_state?
|
||||
tracked_points.none? && trial?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_api_key
|
||||
|
|
@ -133,4 +139,17 @@ class User < ApplicationRecord
|
|||
settings['photoprism_url']&.gsub!(%r{/+\z}, '')
|
||||
settings.try(:[], 'maps')&.try(:[], 'url')&.strip!
|
||||
end
|
||||
|
||||
def start_trial
|
||||
update(status: :trial, active_until: 7.days.from_now)
|
||||
|
||||
Users::TrialWebhookJob.perform_later(id)
|
||||
end
|
||||
|
||||
def schedule_welcome_emails
|
||||
Users::MailerSendingJob.perform_later(id, 'welcome')
|
||||
Users::MailerSendingJob.set(wait: 2.days).perform_later(id, 'explore_features')
|
||||
Users::MailerSendingJob.set(wait: 5.days).perform_later(id, 'trial_expires_soon')
|
||||
Users::MailerSendingJob.set(wait: 7.days).perform_later(id, 'trial_expired')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
16
app/services/subscription/encode_jwt_token.rb
Normal file
16
app/services/subscription/encode_jwt_token.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Subscription::EncodeJwtToken
|
||||
def initialize(payload, secret_key)
|
||||
@payload = payload
|
||||
@secret_key = secret_key
|
||||
end
|
||||
|
||||
def call
|
||||
JWT.encode(
|
||||
@payload,
|
||||
@secret_key,
|
||||
'HS256'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,14 @@
|
|||
<p class="py-6">
|
||||
<p class='py-2'>Use this API key to authenticate your requests.</p>
|
||||
<code><%= current_user.api_key %></code>
|
||||
|
||||
<%# if ENV['QR_CODE_ENABLED'] == 'true' %>
|
||||
<p class='py-2'>
|
||||
Or you can scan it in your Dawarich iOS app:
|
||||
<%= api_key_qr_code(current_user) %>
|
||||
</p>
|
||||
<%# end %>
|
||||
|
||||
<p class='py-2'>
|
||||
<p>Docs: <%= link_to "API documentation", '/api-docs', class: 'underline hover:no-underline' %></p>
|
||||
|
||||
|
|
@ -20,7 +28,6 @@
|
|||
<div class='divider'>OR</div>
|
||||
<h3 class='text-lg font-bold mt-4'>Overland</h3>
|
||||
<p><code><%= api_v1_overland_batches_url(api_key: current_user.api_key) %></code></p>
|
||||
|
||||
</p>
|
||||
<p class='py-2'>
|
||||
<%= link_to "Generate new API key", generate_api_key_path, data: { confirm: "Are you sure? This will invalidate the current API key.", turbo_confirm: "Are you sure?", turbo_method: :post }, class: 'btn btn-primary' %>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@
|
|||
<div class="hero-content flex-col lg:flex-row-reverse w-full my-5">
|
||||
<div class="text-center lg:text-left">
|
||||
<h1 class="text-5xl font-bold mb-5">Edit your account!</h1>
|
||||
<%= render 'devise/registrations/api_key' %>
|
||||
<% if current_user.active? %>
|
||||
<%= render 'devise/registrations/api_key' %>
|
||||
<% else %>
|
||||
<p>
|
||||
<%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success glass' %> to access your API key and start tracking your location.
|
||||
</p>
|
||||
<% end %>
|
||||
<% if !DawarichSettings.self_hosted? %>
|
||||
<%= render 'devise/registrations/points_usage' %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -31,5 +31,7 @@
|
|||
</div>
|
||||
<%= render SELF_HOSTED ? 'shared/footer' : 'shared/legal_footer' %>
|
||||
</div>
|
||||
|
||||
<%= render 'map/onboarding_modal' %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
16
app/views/map/_onboarding_modal.html.erb
Normal file
16
app/views/map/_onboarding_modal.html.erb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<dialog id="getting_started" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Start tracking your location!</h3>
|
||||
<p class="py-4">
|
||||
To start tracking your location and putting it on the map, you need to configure your mobile application.
|
||||
</p>
|
||||
<p>
|
||||
To do so, grab the API key from <%= link_to 'here', settings_path, class: 'link' %> and follow the instructions in the <%= link_to 'documentation', 'https://docs.dawarich.com/mobile-apps/android', class: 'link' %>.
|
||||
</p>
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button class="btn">Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
|
@ -20,7 +20,9 @@
|
|||
</details>
|
||||
</li>
|
||||
<% if user_signed_in? && current_user.can_subscribe? %>
|
||||
<li><%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %></li>
|
||||
<li>
|
||||
<%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -70,9 +72,18 @@
|
|||
<div class="navbar-end">
|
||||
<ul class="menu menu-horizontal bg-base-100 rounded-box px-1">
|
||||
<% if user_signed_in? %>
|
||||
<% if current_user.can_subscribe? %>
|
||||
<li><%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %></li>
|
||||
<% end %>
|
||||
|
||||
<%# if current_user.can_subscribe? %>
|
||||
<div class="join">
|
||||
<%= link_to "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}" do %>
|
||||
<span class="join-item btn btn-sm <%= trial_button_class(current_user) %>">
|
||||
<span class="tooltip tooltip-bottom" data-tip="Your trial will end in <%= distance_of_time_in_words(current_user.active_until, Time.current) %>"><%= (current_user.active_until.to_date - Time.current.to_date).to_i %> days remaining</span>
|
||||
</span><span class="join-item btn btn-sm btn-success">
|
||||
Subscribe
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<%# end %>
|
||||
|
||||
<div class="dropdown dropdown-end dropdown-bottom dropdown-hover"
|
||||
data-controller="notifications"
|
||||
|
|
@ -113,6 +124,9 @@
|
|||
<details>
|
||||
<summary>
|
||||
<%= "#{current_user.email}" %>
|
||||
<% if onboarding_modal_showable?(current_user) %>
|
||||
<span class="indicator-item badge badge-secondary badge-xs"></span>
|
||||
<% end %>
|
||||
<% if current_user.admin? %>
|
||||
<span class='tooltip tooltip-bottom' data-tip="You're an admin, Harry!">⭐️</span>
|
||||
<% end %>
|
||||
|
|
@ -123,6 +137,14 @@
|
|||
<% if !DawarichSettings.self_hosted? %>
|
||||
<li><%= link_to 'Subscription', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}" %></li>
|
||||
<% end %>
|
||||
<li>
|
||||
<a onclick="getting_started.showModal()" class="relative">
|
||||
Get started
|
||||
<% if onboarding_modal_showable?(current_user) %>
|
||||
<span class="indicator-item badge badge-secondary badge-xs"></span>
|
||||
<% end %>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><%= link_to 'Logout', destroy_user_session_path, method: :delete, data: { turbo: false } %></li>
|
||||
</ul>
|
||||
|
|
|
|||
55
app/views/users_mailer/explore_features.html.erb
Normal file
55
app/views/users_mailer/explore_features.html.erb
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: #16a34a; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; background: #f9fafb; }
|
||||
.cta { background: #16a34a; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; }
|
||||
.feature { margin: 15px 0; padding: 15px; background: white; border-left: 4px solid #16a34a; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Explore Dawarich Features</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hi <%= @user.email %>,</p>
|
||||
|
||||
<p>You're now 2 days into your Dawarich trial! We hope you're enjoying tracking your location data.</p>
|
||||
|
||||
<p>Here are some powerful features you might want to explore:</p>
|
||||
|
||||
<div class="feature">
|
||||
<h3>📊 Statistics & Analytics</h3>
|
||||
<p>View detailed insights about distances traveled and time spent in different locations.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<h3>🗺️ Interactive Maps</h3>
|
||||
<p>Visualize your tracks on beautiful maps with different layers and styling options.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<h3>📍 Places & Visits</h3>
|
||||
<p>Discover the places you've visited and get automatic visit detection for frequently visited locations.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature">
|
||||
<h3>📤 Data Export</h3>
|
||||
<p>Export your location data in multiple formats (GPX, GeoJSON) for backup or use with other applications.</p>
|
||||
</div>
|
||||
|
||||
<a href="https://my.dawarich.app" class="cta">Continue Exploring</a>
|
||||
|
||||
<p>You have <strong>5 days</strong> left in your trial. Make the most of it!</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Evgenii from Dawarich</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
app/views/users_mailer/explore_features.text.erb
Normal file
26
app/views/users_mailer/explore_features.text.erb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
Explore Dawarich Features
|
||||
|
||||
Hi <%= @user.email %>,
|
||||
|
||||
You're now 2 days into your Dawarich trial! We hope you're enjoying tracking your location data.
|
||||
|
||||
Here are some powerful features you might want to explore:
|
||||
|
||||
📊 Statistics & Analytics
|
||||
View detailed insights about distances traveled and time spent in different locations.
|
||||
|
||||
🗺️ Interactive Maps
|
||||
Visualize your tracks on beautiful maps with different layers and styling options.
|
||||
|
||||
📍 Places & Visits
|
||||
Discover the places you've visited and get automatic visit detection for frequently visited locations.
|
||||
|
||||
📤 Data Export
|
||||
Export your location data in multiple formats (GPX, GeoJSON) for backup or use with other applications.
|
||||
|
||||
Continue exploring: https://my.dawarich.app
|
||||
|
||||
You have 5 days left in your trial. Make the most of it!
|
||||
|
||||
Best regards,
|
||||
Evgenii from Dawarich
|
||||
50
app/views/users_mailer/trial_expired.html.erb
Normal file
50
app/views/users_mailer/trial_expired.html.erb
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: #dc2626; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; background: #f9fafb; }
|
||||
.cta { background: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; }
|
||||
.expired { background: #fee2e2; border: 1px solid #dc2626; padding: 15px; border-radius: 6px; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔒 Your Trial Has Expired</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hi <%= @user.email %>,</p>
|
||||
|
||||
<div class="expired">
|
||||
<p><strong>Your 7-day Dawarich trial has ended.</strong></p>
|
||||
</div>
|
||||
|
||||
<p>Thank you for trying Dawarich! We hope you enjoyed exploring your location data over the past week.</p>
|
||||
|
||||
<p>Your trial account is now limited, but your data is safe and secure. To regain full access to all features, please subscribe to continue your journey with Dawarich.</p>
|
||||
|
||||
<h3>🔓 Restore full access with a subscription:</h3>
|
||||
<ul>
|
||||
<li>Resume location tracking</li>
|
||||
<li>Access all your historical data</li>
|
||||
<li>Use travel analytics and insights</li>
|
||||
<li>Export data in multiple formats</li>
|
||||
<li>Enjoy beautiful interactive maps</li>
|
||||
</ul>
|
||||
|
||||
<a href="https://my.dawarich.app" class="cta">Subscribe to Continue</a>
|
||||
|
||||
<p>Ready to unlock the full power of location insights? Subscribe now and pick up right where you left off!</p>
|
||||
|
||||
<p>We'd love to have you back as a subscriber.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Evgenii from Dawarich</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
25
app/views/users_mailer/trial_expired.text.erb
Normal file
25
app/views/users_mailer/trial_expired.text.erb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
🔒 Your Trial Has Expired
|
||||
|
||||
Hi <%= @user.email %>,
|
||||
|
||||
Your 7-day Dawarich trial has ended.
|
||||
|
||||
Thank you for trying Dawarich! We hope you enjoyed exploring your location data over the past week.
|
||||
|
||||
Your trial account is now limited, but your data is safe and secure. To regain full access to all features, please subscribe to continue your journey with Dawarich.
|
||||
|
||||
🔓 Restore full access with a subscription:
|
||||
- Resume location tracking
|
||||
- Access all your historical data
|
||||
- Use travel analytics and insights
|
||||
- Export data in multiple formats
|
||||
- Enjoy beautiful interactive maps
|
||||
|
||||
Subscribe to continue: https://my.dawarich.app
|
||||
|
||||
Ready to unlock the full power of location insights? Subscribe now and pick up right where you left off!
|
||||
|
||||
We'd love to have you back as a subscriber.
|
||||
|
||||
Best regards,
|
||||
Evgenii from Dawarich
|
||||
50
app/views/users_mailer/trial_expires_soon.html.erb
Normal file
50
app/views/users_mailer/trial_expires_soon.html.erb
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: #f59e0b; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; background: #f9fafb; }
|
||||
.cta { background: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; }
|
||||
.urgent { background: #fef3c7; border: 1px solid #f59e0b; padding: 15px; border-radius: 6px; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>⏰ Your Trial Expires Soon</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hi <%= @user.email %>,</p>
|
||||
|
||||
<div class="urgent">
|
||||
<p><strong>⚠️ Important:</strong> Your Dawarich trial expires in just <strong>2 days</strong>!</p>
|
||||
</div>
|
||||
|
||||
<p>We hope you've enjoyed exploring your location data with Dawarich over the past 5 days.</p>
|
||||
|
||||
<p>To continue using all of Dawarich's powerful features after your trial ends, you'll need to subscribe to a plan.</p>
|
||||
|
||||
<h3>✨ What you'll keep with a subscription:</h3>
|
||||
<ul>
|
||||
<li>Location tracking and data storage</li>
|
||||
<li>Travel analytics and insights</li>
|
||||
<li>Data export in multiple formats</li>
|
||||
<li>Beautiful interactive maps</li>
|
||||
<li>Visit detection and places management</li>
|
||||
</ul>
|
||||
|
||||
<a href="https://my.dawarich.app" class="cta">Subscribe Now</a>
|
||||
|
||||
<p>Don't lose access to your location insights. Subscribe today and continue your journey with Dawarich!</p>
|
||||
|
||||
<p>Questions? Drop us a message at hi@dawarich.app</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
Evgenii from Dawarich</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
25
app/views/users_mailer/trial_expires_soon.text.erb
Normal file
25
app/views/users_mailer/trial_expires_soon.text.erb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
⏰ Your Trial Expires Soon
|
||||
|
||||
Hi <%= @user.email %>,
|
||||
|
||||
⚠️ Important: Your Dawarich trial expires in just 2 days!
|
||||
|
||||
We hope you've enjoyed exploring your location data with Dawarich over the past 5 days.
|
||||
|
||||
To continue using all of Dawarich's powerful features after your trial ends, you'll need to subscribe to a plan.
|
||||
|
||||
✨ What you'll keep with a subscription:
|
||||
- Location tracking and data storage
|
||||
- Travel analytics and insights
|
||||
- Data export in multiple formats
|
||||
- Beautiful interactive maps
|
||||
- Visit detection and places management
|
||||
|
||||
Subscribe now: https://my.dawarich.app
|
||||
|
||||
Don't lose access to your location insights. Subscribe today and continue your journey with Dawarich!
|
||||
|
||||
Questions? Drop us a message at hi@dawarich.app
|
||||
|
||||
Best regards,
|
||||
Evgenii from Dawarich
|
||||
40
app/views/users_mailer/welcome.html.erb
Normal file
40
app/views/users_mailer/welcome.html.erb
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: #2563eb; color: white; padding: 20px; text-align: center; }
|
||||
.content { padding: 20px; background: #f9fafb; }
|
||||
.cta { background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Welcome to Dawarich!</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hi <%= @user.email %>,</p>
|
||||
|
||||
<p>Welcome to Dawarich! We're excited to have you on board.</p>
|
||||
|
||||
<p>Your 7-day free trial has started. During this time, you can:</p>
|
||||
<ul>
|
||||
<li>Track your location data</li>
|
||||
<li>View your movement patterns on beautiful maps</li>
|
||||
<li>Analyze your travel statistics</li>
|
||||
<li>Export your data in various formats</li>
|
||||
</ul>
|
||||
|
||||
<a href="https://my.dawarich.app" class="cta">Start Exploring Dawarich</a>
|
||||
|
||||
<p>If you have any questions, feel free to drop us a message at hi@dawarich.app</p>
|
||||
|
||||
<p>Happy tracking!<br>
|
||||
Evgenii from Dawarich</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
18
app/views/users_mailer/welcome.text.erb
Normal file
18
app/views/users_mailer/welcome.text.erb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Welcome to Dawarich!
|
||||
|
||||
Hi <%= @user.email %>,
|
||||
|
||||
Welcome to Dawarich! We're excited to have you on board.
|
||||
|
||||
Your 7-day free trial has started. During this time, you can:
|
||||
- Track your location data
|
||||
- View your movement patterns on beautiful maps
|
||||
- Analyze your travel statistics
|
||||
- Export your data in various formats
|
||||
|
||||
Start exploring Dawarich: https://my.dawarich.app
|
||||
|
||||
If you have any questions, feel free to drop us a message at hi@dawarich.app
|
||||
|
||||
Happy tracking!
|
||||
Evgenii from Dawarich
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
- data_migrations
|
||||
- points
|
||||
- default
|
||||
- mailers
|
||||
- imports
|
||||
- exports
|
||||
- stats
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ FactoryBot.define do
|
|||
active_until { 1.day.ago }
|
||||
end
|
||||
|
||||
trait :trial do
|
||||
status { :trial }
|
||||
active_until { 7.days.from_now }
|
||||
end
|
||||
|
||||
trait :with_immich_integration do
|
||||
settings do
|
||||
{
|
||||
|
|
|
|||
3
spec/fixtures/users/welcome
vendored
Normal file
3
spec/fixtures/users/welcome
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Users#welcome
|
||||
|
||||
Hi, find me in app/views/users/welcome
|
||||
75
spec/jobs/users/mailer_sending_job_spec.rb
Normal file
75
spec/jobs/users/mailer_sending_job_spec.rb
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Users::MailerSendingJob, type: :job do
|
||||
let(:user) { create(:user, :trial) }
|
||||
let(:mailer_double) { double('mailer', deliver_later: true) }
|
||||
|
||||
before do
|
||||
allow(UsersMailer).to receive(:with).and_return(UsersMailer)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when email_type is welcome' do
|
||||
it 'sends welcome email' do
|
||||
expect(UsersMailer).to receive(:with).with({ user: user })
|
||||
expect(UsersMailer).to receive(:welcome).and_return(mailer_double)
|
||||
expect(mailer_double).to receive(:deliver_later)
|
||||
|
||||
described_class.perform_now(user.id, 'welcome')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when email_type is explore_features' do
|
||||
it 'sends explore_features email' do
|
||||
expect(UsersMailer).to receive(:with).with({ user: user })
|
||||
expect(UsersMailer).to receive(:explore_features).and_return(mailer_double)
|
||||
expect(mailer_double).to receive(:deliver_later)
|
||||
|
||||
described_class.perform_now(user.id, 'explore_features')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when email_type is trial_expires_soon' do
|
||||
it 'sends trial_expires_soon email' do
|
||||
expect(UsersMailer).to receive(:with).with({ user: user })
|
||||
expect(UsersMailer).to receive(:trial_expires_soon).and_return(mailer_double)
|
||||
expect(mailer_double).to receive(:deliver_later)
|
||||
|
||||
described_class.perform_now(user.id, 'trial_expires_soon')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when email_type is trial_expired' do
|
||||
it 'sends trial_expired email' do
|
||||
expect(UsersMailer).to receive(:with).with({ user: user })
|
||||
expect(UsersMailer).to receive(:trial_expired).and_return(mailer_double)
|
||||
expect(mailer_double).to receive(:deliver_later)
|
||||
|
||||
described_class.perform_now(user.id, 'trial_expired')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with additional options' do
|
||||
it 'merges options with user params' do
|
||||
custom_options = { custom_data: 'test', priority: :high }
|
||||
expected_params = { user: user, custom_data: 'test', priority: :high }
|
||||
|
||||
expect(UsersMailer).to receive(:with).with(expected_params)
|
||||
expect(UsersMailer).to receive(:welcome).and_return(mailer_double)
|
||||
expect(mailer_double).to receive(:deliver_later)
|
||||
|
||||
described_class.perform_now(user.id, 'welcome', **custom_options)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is deleted' do
|
||||
it 'raises ActiveRecord::RecordNotFound' do
|
||||
user.destroy
|
||||
|
||||
expect {
|
||||
described_class.perform_now(user.id, 'welcome')
|
||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
54
spec/jobs/users/trial_webhook_job_spec.rb
Normal file
54
spec/jobs/users/trial_webhook_job_spec.rb
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Users::TrialWebhookJob, type: :job do
|
||||
let(:user) { create(:user, :trial) }
|
||||
let(:jwt_token) { 'encoded.jwt.token' }
|
||||
let(:manager_url) { 'https://manager.example.com' }
|
||||
let(:request_url) { "#{manager_url}/api/v1/users" }
|
||||
let(:jwt_service) { instance_double(Subscription::EncodeJwtToken, call: jwt_token) }
|
||||
|
||||
before do
|
||||
stub_const('ENV', ENV.to_hash.merge('MANAGER_URL' => manager_url, 'JWT_SECRET_KEY' => 'secret'))
|
||||
allow(Subscription::EncodeJwtToken).to receive(:new).and_return(jwt_service)
|
||||
allow(HTTParty).to receive(:post)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'encodes JWT with correct payload' do
|
||||
expected_payload = {
|
||||
user_id: user.id,
|
||||
email: user.email,
|
||||
action: 'create_user'
|
||||
}
|
||||
|
||||
expect(Subscription::EncodeJwtToken).to receive(:new)
|
||||
.with(expected_payload, 'secret')
|
||||
.and_return(jwt_service)
|
||||
|
||||
described_class.perform_now(user.id)
|
||||
end
|
||||
|
||||
it 'makes HTTP POST request to Manager API' do
|
||||
expected_headers = {
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json'
|
||||
}
|
||||
expected_body = { token: jwt_token }.to_json
|
||||
|
||||
expect(HTTParty).to receive(:post)
|
||||
.with(request_url, headers: expected_headers, body: expected_body)
|
||||
|
||||
described_class.perform_now(user.id)
|
||||
end
|
||||
|
||||
context 'when user is deleted' do
|
||||
it 'raises ActiveRecord::RecordNotFound' do
|
||||
user.destroy
|
||||
|
||||
expect {
|
||||
described_class.perform_now(user.id)
|
||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
9
spec/mailers/previews/users_mailer_preview.rb
Normal file
9
spec/mailers/previews/users_mailer_preview.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Preview all emails at http://localhost:3000/rails/mailers/users_mailer
|
||||
class UsersMailerPreview < ActionMailer::Preview
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/users_mailer/welcome
|
||||
def welcome
|
||||
UsersMailer.welcome
|
||||
end
|
||||
|
||||
end
|
||||
49
spec/mailers/users_mailer_spec.rb
Normal file
49
spec/mailers/users_mailer_spec.rb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UsersMailer, type: :mailer do
|
||||
let(:user) { create(:user, email: 'test@example.com') }
|
||||
|
||||
before do
|
||||
stub_const('ENV', ENV.to_hash.merge('SMTP_FROM' => 'hi@dawarich.app'))
|
||||
end
|
||||
|
||||
describe "welcome" do
|
||||
let(:mail) { UsersMailer.with(user: user).welcome }
|
||||
|
||||
it "renders the headers" do
|
||||
expect(mail.subject).to eq("Welcome to Dawarich")
|
||||
expect(mail.to).to eq(["test@example.com"])
|
||||
end
|
||||
|
||||
it "renders the body" do
|
||||
expect(mail.body.encoded).to match("test@example.com")
|
||||
end
|
||||
end
|
||||
|
||||
describe "explore_features" do
|
||||
let(:mail) { UsersMailer.with(user: user).explore_features }
|
||||
|
||||
it "renders the headers" do
|
||||
expect(mail.subject).to eq("Explore Dawarich features")
|
||||
expect(mail.to).to eq(["test@example.com"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "trial_expires_soon" do
|
||||
let(:mail) { UsersMailer.with(user: user).trial_expires_soon }
|
||||
|
||||
it "renders the headers" do
|
||||
expect(mail.subject).to eq("Your Dawarich trial expires in 2 days")
|
||||
expect(mail.to).to eq(["test@example.com"])
|
||||
end
|
||||
end
|
||||
|
||||
describe "trial_expired" do
|
||||
let(:mail) { UsersMailer.with(user: user).trial_expired }
|
||||
|
||||
it "renders the headers" do
|
||||
expect(mail.subject).to eq("Your Dawarich trial expired")
|
||||
expect(mail.to).to eq(["test@example.com"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,7 +18,7 @@ RSpec.describe User, type: :model do
|
|||
end
|
||||
|
||||
describe 'enums' do
|
||||
it { is_expected.to define_enum_for(:status).with_values(inactive: 0, active: 1) }
|
||||
it { is_expected.to define_enum_for(:status).with_values(inactive: 0, active: 1, trial: 3) }
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
|
|
@ -49,19 +49,108 @@ RSpec.describe User, type: :model do
|
|||
allow(DawarichSettings).to receive(:self_hosted?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not activate user' do
|
||||
it 'sets user to trial instead of active' do
|
||||
user = create(:user, :inactive)
|
||||
|
||||
expect(user.active?).to be_falsey
|
||||
expect(user.active_until).to be_within(1.minute).of(1.day.ago)
|
||||
expect(user.trial?).to be_truthy
|
||||
expect(user.active_until).to be_within(1.minute).of(7.days.from_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#start_trial' do
|
||||
let(:user) { create(:user, :inactive) }
|
||||
|
||||
before do
|
||||
allow(Users::TrialWebhookJob).to receive(:perform_later)
|
||||
end
|
||||
|
||||
it 'sets trial status and active_until to 7 days from now' do
|
||||
user.send(:start_trial)
|
||||
|
||||
expect(user.reload.trial?).to be_truthy
|
||||
expect(user.active_until).to be_within(1.minute).of(7.days.from_now)
|
||||
end
|
||||
|
||||
it 'enqueues trial webhook job' do
|
||||
expect(Users::TrialWebhookJob).to receive(:perform_later).with(user.id)
|
||||
user.send(:start_trial)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#schedule_welcome_emails' do
|
||||
let(:user) { create(:user, :inactive) }
|
||||
|
||||
before do
|
||||
allow(Users::MailerSendingJob).to receive(:perform_later)
|
||||
allow(Users::MailerSendingJob).to receive(:set).and_return(Users::MailerSendingJob)
|
||||
end
|
||||
|
||||
it 'schedules welcome email immediately' do
|
||||
expect(Users::MailerSendingJob).to receive(:perform_later).with(user.id, 'welcome')
|
||||
user.send(:schedule_welcome_emails)
|
||||
end
|
||||
|
||||
it 'schedules explore_features email for day 2' do
|
||||
expect(Users::MailerSendingJob).to receive(:set).with(wait: 2.days).and_return(Users::MailerSendingJob)
|
||||
expect(Users::MailerSendingJob).to receive(:perform_later).with(user.id, 'explore_features')
|
||||
user.send(:schedule_welcome_emails)
|
||||
end
|
||||
|
||||
it 'schedules trial_expires_soon email for day 5' do
|
||||
expect(Users::MailerSendingJob).to receive(:set).with(wait: 5.days).and_return(Users::MailerSendingJob)
|
||||
expect(Users::MailerSendingJob).to receive(:perform_later).with(user.id, 'trial_expires_soon')
|
||||
user.send(:schedule_welcome_emails)
|
||||
end
|
||||
|
||||
it 'schedules trial_expired email for day 7' do
|
||||
expect(Users::MailerSendingJob).to receive(:set).with(wait: 7.days).and_return(Users::MailerSendingJob)
|
||||
expect(Users::MailerSendingJob).to receive(:perform_later).with(user.id, 'trial_expired')
|
||||
user.send(:schedule_welcome_emails)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'methods' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe '#trial_state?' do
|
||||
context 'when user has trial status and no tracked points' do
|
||||
let(:user) do
|
||||
user = build(:user, :trial)
|
||||
user.save!(validate: false)
|
||||
user.update_column(:status, 'trial')
|
||||
user
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
user.tracked_points.destroy_all
|
||||
|
||||
expect(user.trial_state?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has trial status but has tracked points' do
|
||||
let(:user) { create(:user, :trial) }
|
||||
|
||||
before do
|
||||
create(:point, user: user)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(user.trial_state?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not on trial' do
|
||||
let(:user) { create(:user, :active) }
|
||||
|
||||
it 'returns false' do
|
||||
expect(user.trial_state?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#countries_visited' do
|
||||
subject { user.countries_visited }
|
||||
|
||||
|
|
@ -204,7 +293,12 @@ RSpec.describe User, type: :model do
|
|||
end
|
||||
|
||||
context 'when user is inactive' do
|
||||
let(:user) { create(:user, :inactive) }
|
||||
let(:user) do
|
||||
user = build(:user, :inactive)
|
||||
user.save!(validate: false)
|
||||
user.update_columns(status: 'inactive', active_until: 1.day.ago)
|
||||
user
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(user.can_subscribe?).to be_truthy
|
||||
|
|
|
|||
30
spec/services/subscription/encode_jwt_token_spec.rb
Normal file
30
spec/services/subscription/encode_jwt_token_spec.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Subscription::EncodeJwtToken do
|
||||
let(:payload) { { user_id: 123, email: 'test@example.com', action: 'create_user' } }
|
||||
let(:secret_key) { 'secret_key' }
|
||||
let(:service) { described_class.new(payload, secret_key) }
|
||||
|
||||
describe '#call' do
|
||||
it 'encodes JWT with correct algorithm' do
|
||||
expect(JWT).to receive(:encode)
|
||||
.with(payload, secret_key, 'HS256')
|
||||
.and_return('encoded.jwt.token')
|
||||
|
||||
result = service.call
|
||||
expect(result).to eq('encoded.jwt.token')
|
||||
end
|
||||
|
||||
it 'returns encoded JWT token' do
|
||||
token = service.call
|
||||
|
||||
decoded_payload = JWT.decode(token, secret_key, 'HS256').first
|
||||
|
||||
expect(decoded_payload['user_id']).to eq(123)
|
||||
expect(decoded_payload['email']).to eq('test@example.com')
|
||||
expect(decoded_payload['action']).to eq('create_user')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
# Dawarich System Test Scenarios
|
||||
|
||||
This document tracks all system test scenarios for the Dawarich application. Completed scenarios are marked with `[x]` and pending scenarios with `[ ]`.
|
||||
|
||||
## 1. Authentication & User Management
|
||||
|
||||
### Sign In/Out
|
||||
- [x] User can sign in with valid credentials
|
||||
- [x] User is redirected to map page after successful sign in
|
||||
- [x] User cannot sign in with invalid credentials
|
||||
- [x] User can sign out successfully
|
||||
- [x] User is redirected to sign in page when accessing protected routes while signed out
|
||||
|
||||
### User Registration
|
||||
- [ ] New user can register with valid information
|
||||
- [ ] Registration fails with invalid email format
|
||||
- [ ] Registration fails with weak password
|
||||
- [ ] Registration fails with mismatched password confirmation
|
||||
- [ ] Email confirmation process works correctly
|
||||
|
||||
### Password Management
|
||||
- [ ] User can request password reset
|
||||
- [ ] Password reset email is sent
|
||||
- [ ] User can reset password with valid token
|
||||
- [ ] Password reset fails with expired token
|
||||
- [ ] User can change password when signed in
|
||||
|
||||
## 2. Map Functionality
|
||||
|
||||
### Basic Map Operations
|
||||
- [x] Leaflet map initializes correctly
|
||||
- [x] Map displays with proper container and panes
|
||||
- [x] Map tiles load successfully
|
||||
- [x] Zoom in/out functionality works
|
||||
- [x] Map controls are present and functional
|
||||
|
||||
### Map Layers
|
||||
- [x] Base layer switching (OpenStreetMap ↔ OpenTopo)
|
||||
- [x] Layer control expands and collapses
|
||||
- [x] Overlay layers can be toggled (Points, Routes, Fog of War, Heatmap, etc.)
|
||||
- [x] Layer states persist after settings updates
|
||||
- [ ] Fallback map layer when preferred layer fails
|
||||
- [ ] Custom tile layer configuration
|
||||
- [ ] Layer loading error handling
|
||||
|
||||
### Map Data Display
|
||||
- [x] Route data loads and displays
|
||||
- [x] Point markers appear on map
|
||||
- [x] Map statistics display (distance, points count)
|
||||
- [x] Map scale control shows correctly
|
||||
- [x] Map attributions are present
|
||||
|
||||
## 3. Route Management
|
||||
|
||||
### Route Display
|
||||
- [x] Routes render as polylines
|
||||
- [x] Route opacity can be adjusted
|
||||
- [x] Speed-colored routes toggle works
|
||||
- [x] Route splitting settings can be configured
|
||||
|
||||
### Route Interaction
|
||||
- [x] Route popup displays on hover/click (basic structure)
|
||||
- [x] Popup shows start/end times, duration, distance, speed
|
||||
- [x] Distance units convert properly (km ↔ miles)
|
||||
- [x] Speed units convert properly (km/h ↔ mph)
|
||||
- [ ] Route deletion with confirmation (not implemented yet)
|
||||
- [ ] Route merging/splitting operations (not implemented yet)
|
||||
- [ ] Route export functionality (not implemented yet)
|
||||
|
||||
## 4. Point Management
|
||||
|
||||
### Point Display
|
||||
- [x] Points display as markers
|
||||
- [x] Point popups show detailed information
|
||||
- [x] Point rendering mode can be toggled (raw/simplified)
|
||||
|
||||
### Point Operations
|
||||
- [x] Point deletion link is present and functional
|
||||
- [ ] Point deletion confirmation dialog
|
||||
- [ ] Point editing (coordinates via drag and drop)
|
||||
- [ ] Point filtering by date/time
|
||||
|
||||
## 5. Settings Panel
|
||||
|
||||
### Map Settings
|
||||
- [x] Settings panel opens and closes
|
||||
- [x] Route opacity updates
|
||||
- [x] Fog of war settings (radius, threshold)
|
||||
- [x] Route splitting configuration (meters, minutes)
|
||||
- [x] Points rendering mode toggle
|
||||
- [x] Live map functionality toggle
|
||||
- [x] Speed-colored routes toggle
|
||||
- [x] Speed color scale updates
|
||||
- [x] Gradient editor modal interaction
|
||||
|
||||
### Settings Validation
|
||||
- [ ] Invalid settings values are rejected
|
||||
- [ ] Settings form validation messages
|
||||
- [ ] Settings reset to defaults
|
||||
- [ ] Settings import/export functionality
|
||||
|
||||
## 6. Calendar Panel
|
||||
|
||||
### Calendar Display
|
||||
- [x] Calendar button is functional
|
||||
- [x] Calendar panel opens and displays correctly
|
||||
- [ ] Year selection works
|
||||
- [ ] Month navigation functions
|
||||
- [ ] Visited cities information displays
|
||||
|
||||
### Calendar Interaction
|
||||
- [ ] Date selection filters map data
|
||||
- [x] Calendar state persists in localStorage
|
||||
- [ ] Calendar navigation with keyboard shortcuts (not implemented yet)
|
||||
|
||||
## 7. Data Import/Export
|
||||
|
||||
### Import Functionality
|
||||
- [ ] GPX file import
|
||||
- [ ] JSON data import
|
||||
- [ ] .rec file import
|
||||
- [ ] Import validation and error handling
|
||||
- [ ] Import progress indication
|
||||
- [ ] Duplicate data handling during import
|
||||
|
||||
### Export Functionality
|
||||
- [ ] GPX file export
|
||||
- [ ] JSON data export
|
||||
- [ ] Date range export filtering
|
||||
- [ ] Export progress indication
|
||||
|
||||
## 8. Statistics & Analytics
|
||||
|
||||
### Statistics Display
|
||||
- [x] Map statistics show distance and points
|
||||
- [ ] Detailed statistics page
|
||||
- [ ] Distance traveled by time period
|
||||
- [ ] Speed analytics
|
||||
- [ ] Location frequency analysis
|
||||
- [ ] Activity patterns visualization
|
||||
|
||||
### Charts & Visualizations
|
||||
- [ ] Distance over time charts
|
||||
- [ ] Speed distribution charts
|
||||
- [ ] Heatmap visualization
|
||||
- [ ] Activity timeline
|
||||
- [ ] Geographic distribution charts
|
||||
|
||||
## 9. Photos & Media
|
||||
|
||||
### Photo Management
|
||||
- [ ] Photo display on map
|
||||
- [ ] Photo popup with details
|
||||
|
||||
## 10. Areas & Geofencing
|
||||
|
||||
### Area Management
|
||||
- [ ] Create new areas
|
||||
- [ ] Edit existing areas
|
||||
- [ ] Delete areas
|
||||
- [ ] Area visualization on map
|
||||
|
||||
### Area Functionality
|
||||
- [ ] Time spent in areas calculation
|
||||
- [ ] Area visit history
|
||||
- [ ] Area-based filtering
|
||||
|
||||
## 11. Performance & Error Handling
|
||||
|
||||
### Performance Testing
|
||||
- [x] Large dataset handling without crashes
|
||||
- [x] Memory cleanup on page navigation
|
||||
- [ ] Tile monitoring functionality
|
||||
- [ ] Map rendering performance with many points
|
||||
- [ ] Data loading optimization
|
||||
|
||||
### Error Handling
|
||||
- [x] Empty markers array handling
|
||||
- [x] Missing user settings gracefully handled
|
||||
- [ ] Network connectivity issues
|
||||
- [ ] Failed API calls handling
|
||||
- [ ] Invalid coordinates handling
|
||||
- [ ] Database connection errors
|
||||
- [ ] File upload errors
|
||||
|
||||
## 12. User Preferences & Persistence
|
||||
|
||||
### Preference Management
|
||||
- [x] Distance unit preferences (km/miles)
|
||||
- [ ] Preferred map layer persistence
|
||||
- [x] Panel state persistence (basic)
|
||||
- [ ] Theme preferences (light/dark mode)
|
||||
- [ ] Timezone settings (not implemented yet)
|
||||
|
||||
### Data Persistence
|
||||
- [ ] Map view state persistence (zoom, center)
|
||||
- [ ] Filter preferences persistence
|
||||
|
||||
## 13. API Integration
|
||||
|
||||
### External APIs
|
||||
- [x] GitHub API integration (version checking)
|
||||
- [ ] Reverse geocoding functionality
|
||||
|
||||
### API Error Handling
|
||||
- [x] GitHub API stub for testing
|
||||
- [ ] API rate limiting handling
|
||||
- [ ] API timeout handling
|
||||
- [ ] Fallback when APIs are unavailable
|
||||
|
||||
## 14. Mobile Responsiveness
|
||||
|
||||
### Mobile Layout
|
||||
- [ ] Map displays correctly on mobile devices
|
||||
- [ ] Touch gestures work (pinch to zoom, pan)
|
||||
- [ ] Mobile-optimized controls
|
||||
- [ ] Responsive navigation menu
|
||||
|
||||
## 15. Security & Privacy
|
||||
|
||||
### Data Security
|
||||
- [ ] User data isolation (users only see their own data)
|
||||
- [ ] Secure file upload validation
|
||||
- [ ] XSS protection in user inputs
|
||||
- [ ] CSRF protection on forms
|
||||
|
||||
### Privacy Features
|
||||
- [ ] Data anonymization options
|
||||
- [ ] Location data privacy settings
|
||||
- [ ] Data deletion functionality
|
||||
- [ ] Privacy policy compliance
|
||||
|
||||
## 16. Accessibility
|
||||
|
||||
### WCAG Compliance
|
||||
- [ ] Keyboard navigation support
|
||||
- [ ] Screen reader compatibility
|
||||
- [ ] High contrast mode support
|
||||
- [ ] Focus indicators on interactive elements
|
||||
|
||||
### Usability
|
||||
- [ ] Tooltips and help text
|
||||
- [ ] Error message clarity
|
||||
- [ ] Loading states and progress indicators
|
||||
- [ ] Consistent UI patterns
|
||||
|
||||
## 17. Integration Testing
|
||||
|
||||
### Database Operations
|
||||
- [ ] Data migration testing
|
||||
- [ ] Backup and restore functionality
|
||||
- [ ] Database performance with large datasets
|
||||
- [ ] Concurrent user operations
|
||||
|
||||
## 18. Navigation & UI
|
||||
|
||||
### Main Navigation
|
||||
- [ ] Navigation menu functionality
|
||||
- [ ] Page transitions work smoothly
|
||||
- [ ] Back/forward browser navigation
|
||||
|
||||
## 19. Trips & Journey Management
|
||||
|
||||
### Trip Creation
|
||||
- [ ] Automatic trip detection (not implemented yet)
|
||||
- [ ] Manual trip creation
|
||||
- [ ] Trip editing (name, description, dates)
|
||||
- [ ] Trip deletion with confirmation
|
||||
|
||||
### Trip Display
|
||||
- [ ] Trip list view
|
||||
- [ ] Trip detail view
|
||||
- [ ] Trip statistics
|
||||
- [ ] Trip sharing functionality (not implemented yet)
|
||||
|
||||
## 21. Notifications & Alerts
|
||||
|
||||
### System Notifications
|
||||
- [x] Success message display
|
||||
- [ ] Error message display
|
||||
- [ ] Warning notifications
|
||||
- [ ] Info notifications
|
||||
|
||||
### User Notifications
|
||||
- [ ] Email notifications for important events
|
||||
|
||||
## 20. Search & Filtering
|
||||
|
||||
### Search Functionality
|
||||
- [ ] Global search across all data
|
||||
- [ ] Location-based search
|
||||
- [ ] Date range search
|
||||
- [ ] Advanced search filters
|
||||
|
||||
### Data Filtering
|
||||
- [ ] Filter by date range
|
||||
- [ ] Filter by location/area
|
||||
- [ ] Filter by activity type
|
||||
- [ ] Filter by speed/distance
|
||||
|
||||
## 21. Backup & Data Management
|
||||
|
||||
### Data Backup
|
||||
- [ ] Manual data backup
|
||||
- [ ] Backup verification
|
||||
- [ ] Backup restoration
|
||||
|
||||
### Data Cleanup
|
||||
- [ ] Duplicate data detection
|
||||
- [ ] Data archiving
|
||||
- [ ] Data purging (old data)
|
||||
- [ ] Storage optimization
|
||||
|
||||
---
|
||||
|
||||
## Test Execution Summary
|
||||
|
||||
**Total Scenarios:** 180+
|
||||
**Completed:** 51 ✅
|
||||
**Pending:** 129+ ⏳
|
||||
**Coverage:** ~28%
|
||||
|
||||
### Priority for Next Implementation:
|
||||
1. **Authentication flows** (sign out, invalid credentials, registration)
|
||||
2. **Error handling** (network issues, invalid data, API failures)
|
||||
3. **Calendar panel JavaScript interactions**
|
||||
4. **Data import/export functionality**
|
||||
5. **Mobile responsiveness testing**
|
||||
6. **Security & privacy features**
|
||||
7. **Performance optimization tests**
|
||||
8. **Navigation & UI consistency**
|
||||
|
||||
### High-Impact Areas to Focus On:
|
||||
- **User Authentication & Security** - Critical for production use
|
||||
- **Data Import/Export** - Core functionality for user data management
|
||||
- **Error Handling** - Essential for robust application behavior
|
||||
- **Mobile Experience** - Important for modern web applications
|
||||
- **Performance** - Critical for user experience with large datasets
|
||||
|
||||
### Testing Strategy Notes:
|
||||
- **System Tests**: Focus on user workflows and integration
|
||||
- **Unit Tests**: Cover individual components and business logic
|
||||
- **API Tests**: Ensure robust API behavior and error handling
|
||||
- **Performance Tests**: Validate application behavior under load
|
||||
- **Security Tests**: Verify data protection and access controls
|
||||
|
||||
### Tools & Frameworks:
|
||||
- **RSpec + Capybara**: System/integration testing
|
||||
- **Selenium WebDriver**: Browser automation
|
||||
- **WebMock**: External API mocking
|
||||
- **FactoryBot**: Test data generation
|
||||
- **SimpleCov**: Code coverage analysis
|
||||
28
trials_feature_checklist.md
Normal file
28
trials_feature_checklist.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Trials Feature Checklist
|
||||
|
||||
## ✅ Already Implemented
|
||||
|
||||
- [x] **7-day trial activation** - `set_trial` method sets `status: :trial` and `active_until: 7.days.from_now`
|
||||
- [x] **Welcome email** - Sent immediately after registration
|
||||
- [x] **Scheduled emails** - Feature exploration (day 2), trial expires soon (day 5), trial expired (day 7)
|
||||
- [x] **Trial status enum** - `{ inactive: 0, active: 1, trial: 3 }`
|
||||
- [x] **Navbar Trial Display** - Show number of days left in trial at subscribe button
|
||||
- [x] **Account Deletion Cleanup** - User deletes account during trial, cleanup scheduled emails
|
||||
- [x] Worker to not send emails if user is deleted
|
||||
|
||||
## ❌ Missing/TODO Items
|
||||
|
||||
### Core Requirements
|
||||
- [x] **Specs** - Add specs for all implemented features
|
||||
- [x] User model trial callbacks and methods
|
||||
- [x] Trial webhook job with JWT encoding
|
||||
- [x] Mailer sending job for all email types
|
||||
- [x] JWT encoding service
|
||||
|
||||
|
||||
## Manager (separate application)
|
||||
- [ ] **Manager Webhook** - Create user in Manager service after registration
|
||||
- [ ] **Manager callback** - Manager should daily check user statuses and once trial is expired, update user status to inactive in Dawarich
|
||||
- [ ] **Trial Credit** - Should trial time be credited to first paid month?
|
||||
- [ ] Yes, Manager after payment adds subscription duration to user's active_until
|
||||
- [ ] **User Reactivation** - Handle user returning after trial expired
|
||||
Loading…
Reference in a new issue