Add trial mode

This commit is contained in:
Eugene Burmakin 2025-08-13 20:25:48 +02:00
parent f6b7652a01
commit 71488c9fb1
34 changed files with 848 additions and 371 deletions

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -48,14 +48,14 @@ module ApplicationHelper
grouped_by_country[country] ||= []
if toponym['cities'].present?
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
end
grouped_by_country.transform_values!(&:uniq)
@ -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

View 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

View 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

View 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

View 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

View file

@ -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

View 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

View file

@ -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' %>

View file

@ -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>
<% 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 %>

View file

@ -31,5 +31,7 @@
</div>
<%= render SELF_HOSTED ? 'shared/footer' : 'shared/legal_footer' %>
</div>
<%= render 'map/onboarding_modal' %>
</body>
</html>

View 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>

View file

@ -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>
<%# 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>

View 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>

View 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

View 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>

View 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

View 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>

View 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

View 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>

View 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

View file

@ -4,6 +4,7 @@
- data_migrations
- points
- default
- mailers
- imports
- exports
- stats

View file

@ -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
View file

@ -0,0 +1,3 @@
Users#welcome
Hi, find me in app/views/users/welcome

View 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

View 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

View 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

View 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

View file

@ -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

View 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

View file

@ -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

View 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