From 74112c0d04ae1e14eb573ca2d34f07c19cd5ca0d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 2 Aug 2025 00:06:09 +0200 Subject: [PATCH 01/12] Calculate trip's visited countries from points --- app/models/concerns/calculateable.rb | 1 + app/models/trip.rb | 13 +- app/views/trips/_countries.html.erb | 5 +- .../trips/calculate_countries_job_spec.rb | 114 ++++++++++++++++++ spec/models/trip_spec.rb | 28 ----- 5 files changed, 117 insertions(+), 44 deletions(-) create mode 100644 spec/jobs/trips/calculate_countries_job_spec.rb diff --git a/app/models/concerns/calculateable.rb b/app/models/concerns/calculateable.rb index 31e4ff53..12caeac2 100644 --- a/app/models/concerns/calculateable.rb +++ b/app/models/concerns/calculateable.rb @@ -10,6 +10,7 @@ module Calculateable def calculate_distance calculated_distance_meters = calculate_distance_from_coordinates + self.distance = convert_distance_for_storage(calculated_distance_meters) end diff --git a/app/models/trip.rb b/app/models/trip.rb index 7ba14ad5..e409a47b 100644 --- a/app/models/trip.rb +++ b/app/models/trip.rb @@ -21,12 +21,6 @@ class Trip < ApplicationRecord user.tracked_points.where(timestamp: started_at.to_i..ended_at.to_i).order(:timestamp) end - def countries - return points.pluck(:country).uniq.compact if DawarichSettings.store_geodata? - - visited_countries - end - def photo_previews @photo_previews ||= select_dominant_orientation(photos).sample(12) end @@ -35,13 +29,8 @@ class Trip < ApplicationRecord @photo_sources ||= photos.map { _1[:source] }.uniq end - - def calculate_countries - countries = - Country.where(id: points.pluck(:country_id).compact.uniq).pluck(:name) - - self.visited_countries = countries + self.visited_countries = points.pluck(:country_name).uniq.compact end private diff --git a/app/views/trips/_countries.html.erb b/app/views/trips/_countries.html.erb index 0ae8f7e5..ce6f3c7c 100644 --- a/app/views/trips/_countries.html.erb +++ b/app/views/trips/_countries.html.erb @@ -15,12 +15,9 @@
Countries
- <% if trip.countries.any? %> - <%= trip.countries.join(', ') %> - <% elsif trip.visited_countries.present? %> + <% if trip.visited_countries.any? %> <%= trip.visited_countries.join(', ') %> <% else %> - Countries are being calculated... <% end %>
diff --git a/spec/jobs/trips/calculate_countries_job_spec.rb b/spec/jobs/trips/calculate_countries_job_spec.rb new file mode 100644 index 00000000..d6d8abaa --- /dev/null +++ b/spec/jobs/trips/calculate_countries_job_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Trips::CalculateCountriesJob, type: :job do + describe '#perform' do + let(:user) { create(:user) } + let(:trip) { create(:trip, user: user) } + let(:distance_unit) { 'km' } + let(:points) do + [ + create(:point, user: user, country_name: 'Germany', timestamp: trip.started_at.to_i + 1.hour), + create(:point, user: user, country_name: 'France', timestamp: trip.started_at.to_i + 2.hours), + create(:point, user: user, country_name: 'Germany', timestamp: trip.started_at.to_i + 3.hours), + create(:point, user: user, country_name: 'Italy', timestamp: trip.started_at.to_i + 4.hours) + ] + end + + before do + points # Create the points + end + + it 'finds the trip and calculates countries' do + expect(Trip).to receive(:find).with(trip.id).and_return(trip) + expect(trip).to receive(:calculate_countries) + expect(trip).to receive(:save!) + + described_class.perform_now(trip.id, distance_unit) + end + + it 'calculates unique countries from trip points' do + described_class.perform_now(trip.id, distance_unit) + + trip.reload + expect(trip.visited_countries).to contain_exactly('Germany', 'France', 'Italy') + end + + it 'broadcasts the update with correct parameters' do + expect(Turbo::StreamsChannel).to receive(:broadcast_update_to).with( + "trip_#{trip.id}", + target: "trip_countries", + partial: "trips/countries", + locals: { trip: trip, distance_unit: distance_unit } + ) + + described_class.perform_now(trip.id, distance_unit) + end + + context 'when trip has no points' do + let(:trip_without_points) { create(:trip, user: user) } + + it 'sets visited_countries to empty array' do + trip_without_points.points.destroy_all + described_class.perform_now(trip_without_points.id, distance_unit) + + trip_without_points.reload + + expect(trip_without_points.visited_countries).to eq([]) + end + end + + context 'when points have nil country names' do + let(:points_with_nil_countries) do + [ + create(:point, user: user, country_name: 'Germany', timestamp: trip.started_at.to_i + 1.hour), + create(:point, user: user, country_name: nil, timestamp: trip.started_at.to_i + 2.hours), + create(:point, user: user, country_name: 'France', timestamp: trip.started_at.to_i + 3.hours) + ] + end + + before do + # Remove existing points and create new ones with nil countries + Point.where(user: user).destroy_all + points_with_nil_countries + end + + it 'filters out nil country names' do + described_class.perform_now(trip.id, distance_unit) + + trip.reload + expect(trip.visited_countries).to contain_exactly('Germany', 'France') + end + end + + context 'when trip is not found' do + it 'raises ActiveRecord::RecordNotFound' do + expect { + described_class.perform_now(999999, distance_unit) + }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when distance_unit is different' do + let(:distance_unit) { 'mi' } + + it 'passes the correct distance_unit to broadcast' do + expect(Turbo::StreamsChannel).to receive(:broadcast_update_to).with( + "trip_#{trip.id}", + target: "trip_countries", + partial: "trips/countries", + locals: { trip: trip, distance_unit: 'mi' } + ) + + described_class.perform_now(trip.id, distance_unit) + end + end + + describe 'queue configuration' do + it 'uses the trips queue' do + expect(described_class.queue_name).to eq('trips') + end + end + end +end diff --git a/spec/models/trip_spec.rb b/spec/models/trip_spec.rb index 20bb5ba3..8c46a65a 100644 --- a/spec/models/trip_spec.rb +++ b/spec/models/trip_spec.rb @@ -26,34 +26,6 @@ RSpec.describe Trip, type: :model do trip.save end end - - context 'when DawarichSettings.store_geodata? is enabled' do - before do - allow(DawarichSettings).to receive(:store_geodata?).and_return(true) - end - - it 'sets the countries' do - expect(trip.countries).to eq(trip.points.pluck(:country).uniq.compact) - end - end - end - - describe '#countries' do - let(:user) { create(:user) } - let(:trip) { create(:trip, user:) } - let(:points) do - create_list( - :point, - 25, - :reverse_geocoded, - user:, - timestamp: (trip.started_at.to_i..trip.ended_at.to_i).to_a.sample - ) - end - - it 'returns the unique countries of the points' do - expect(trip.countries).to eq(trip.points.pluck(:country).uniq.compact) - end end describe '#photo_previews' do From 542fb943754c17596eb2067b6d126f1f05a7d13d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:31:03 +0000 Subject: [PATCH 02/12] Bump puma from 6.6.0 to 6.6.1 Bumps [puma](https://github.com/puma/puma) from 6.6.0 to 6.6.1. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v6.6.0...v6.6.1) --- updated-dependencies: - dependency-name: puma dependency-version: 6.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4b955b5a..a0004c16 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -297,7 +297,7 @@ GEM date stringio public_suffix (6.0.1) - puma (6.6.0) + puma (6.6.1) nio4r (~> 2.0) pundit (2.5.0) activesupport (>= 3.0.0) From f6b7652a011392c955654a5c873caa08d396f88d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 11 Aug 2025 00:21:58 +0200 Subject: [PATCH 03/12] Return dawarich headers on all API responses --- CHANGELOG.md | 6 ++++++ app/controllers/api/v1/health_controller.rb | 8 -------- app/controllers/api_controller.rb | 8 ++++++++ spec/requests/api/v1/countries/borders_spec.rb | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f554a6..62a6aa37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +# [0.30.9] - 2025-08-11 + +## Added + +- X-Dawarich-Response and X-Dawarich-Version headers are now returned for all API responses. + # [0.30.8] - 2025-08-01 ## Fixed diff --git a/app/controllers/api/v1/health_controller.rb b/app/controllers/api/v1/health_controller.rb index 8e13d165..1e5ab2f1 100644 --- a/app/controllers/api/v1/health_controller.rb +++ b/app/controllers/api/v1/health_controller.rb @@ -4,14 +4,6 @@ class Api::V1::HealthController < ApiController skip_before_action :authenticate_api_key def index - if current_api_user - response.set_header('X-Dawarich-Response', 'Hey, I\'m alive and authenticated!') - else - response.set_header('X-Dawarich-Response', 'Hey, I\'m alive!') - end - - response.set_header('X-Dawarich-Version', APP_VERSION) - render json: { status: 'ok' } end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 4d13bdaf..d53f57ae 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -2,10 +2,18 @@ class ApiController < ApplicationController skip_before_action :verify_authenticity_token + before_action :set_version_header before_action :authenticate_api_key private + def set_version_header + message = "Hey, I\'m alive#{current_api_user ? ' and authenticated' : ''}!" + + response.set_header('X-Dawarich-Response', message) + response.set_header('X-Dawarich-Version', APP_VERSION) + end + def authenticate_api_key return head :unauthorized unless current_api_user diff --git a/spec/requests/api/v1/countries/borders_spec.rb b/spec/requests/api/v1/countries/borders_spec.rb index d0717dcf..b5922b73 100644 --- a/spec/requests/api/v1/countries/borders_spec.rb +++ b/spec/requests/api/v1/countries/borders_spec.rb @@ -12,6 +12,13 @@ RSpec.describe 'Api::V1::Countries::Borders', type: :request do expect(response).to have_http_status(:unauthorized) end + + it 'returns X-Dawarich-Response header' do + get '/api/v1/countries/borders' + + expect(response.headers['X-Dawarich-Response']).to eq('Hey, I\'m alive!') + expect(response.headers['X-Dawarich-Version']).to eq(APP_VERSION) + end end context 'when user is authenticated' do @@ -22,6 +29,13 @@ RSpec.describe 'Api::V1::Countries::Borders', type: :request do expect(response.body).to include('AF') expect(response.body).to include('ZW') end + + it 'returns X-Dawarich-Response header' do + get '/api/v1/countries/borders', headers: { 'Authorization' => "Bearer #{user.api_key}" } + + expect(response.headers['X-Dawarich-Response']).to eq('Hey, I\'m alive and authenticated!') + expect(response.headers['X-Dawarich-Version']).to eq(APP_VERSION) + end end end end From 71488c9fb1d0c8ceaa64ec7fab0456eb2475d194 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 13 Aug 2025 20:25:48 +0200 Subject: [PATCH 04/12] Add trial mode --- CHANGELOG.md | 1 + Gemfile | 3 +- Gemfile.lock | 6 + app/helpers/application_helper.rb | 27 +- app/helpers/user_helper.rb | 17 + app/jobs/users/mailer_sending_job.rb | 13 + app/jobs/users/trial_webhook_job.rb | 22 ++ app/mailers/users_mailer.rb | 27 ++ app/models/user.rb | 23 +- app/services/subscription/encode_jwt_token.rb | 16 + .../devise/registrations/_api_key.html.erb | 9 +- app/views/devise/registrations/edit.html.erb | 8 +- app/views/layouts/application.html.erb | 2 + app/views/map/_onboarding_modal.html.erb | 16 + app/views/shared/_navbar.html.erb | 30 +- .../users_mailer/explore_features.html.erb | 55 +++ .../users_mailer/explore_features.text.erb | 26 ++ app/views/users_mailer/trial_expired.html.erb | 50 +++ app/views/users_mailer/trial_expired.text.erb | 25 ++ .../users_mailer/trial_expires_soon.html.erb | 50 +++ .../users_mailer/trial_expires_soon.text.erb | 25 ++ app/views/users_mailer/welcome.html.erb | 40 ++ app/views/users_mailer/welcome.text.erb | 18 + config/sidekiq.yml | 1 + spec/factories/users.rb | 5 + spec/fixtures/users/welcome | 3 + spec/jobs/users/mailer_sending_job_spec.rb | 75 ++++ spec/jobs/users/trial_webhook_job_spec.rb | 54 +++ spec/mailers/previews/users_mailer_preview.rb | 9 + spec/mailers/users_mailer_spec.rb | 49 +++ spec/models/user_spec.rb | 104 +++++- .../subscription/encode_jwt_token_spec.rb | 30 ++ tests/system/test_scenarios.md | 352 ------------------ trials_feature_checklist.md | 28 ++ 34 files changed, 848 insertions(+), 371 deletions(-) create mode 100644 app/helpers/user_helper.rb create mode 100644 app/jobs/users/mailer_sending_job.rb create mode 100644 app/jobs/users/trial_webhook_job.rb create mode 100644 app/mailers/users_mailer.rb create mode 100644 app/services/subscription/encode_jwt_token.rb create mode 100644 app/views/map/_onboarding_modal.html.erb create mode 100644 app/views/users_mailer/explore_features.html.erb create mode 100644 app/views/users_mailer/explore_features.text.erb create mode 100644 app/views/users_mailer/trial_expired.html.erb create mode 100644 app/views/users_mailer/trial_expired.text.erb create mode 100644 app/views/users_mailer/trial_expires_soon.html.erb create mode 100644 app/views/users_mailer/trial_expires_soon.text.erb create mode 100644 app/views/users_mailer/welcome.html.erb create mode 100644 app/views/users_mailer/welcome.text.erb create mode 100644 spec/fixtures/users/welcome create mode 100644 spec/jobs/users/mailer_sending_job_spec.rb create mode 100644 spec/jobs/users/trial_webhook_job_spec.rb create mode 100644 spec/mailers/previews/users_mailer_preview.rb create mode 100644 spec/mailers/users_mailer_spec.rb create mode 100644 spec/services/subscription/encode_jwt_token_spec.rb delete mode 100644 tests/system/test_scenarios.md create mode 100644 trials_feature_checklist.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a6aa37..1f721b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Gemfile b/Gemfile index 614a2e95..c7145245 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index 4b955b5a..08c2c161 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index dfd93042..5fdcd917 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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 diff --git a/app/helpers/user_helper.rb b/app/helpers/user_helper.rb new file mode 100644 index 00000000..b28f55b9 --- /dev/null +++ b/app/helpers/user_helper.rb @@ -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 diff --git a/app/jobs/users/mailer_sending_job.rb b/app/jobs/users/mailer_sending_job.rb new file mode 100644 index 00000000..4b7db707 --- /dev/null +++ b/app/jobs/users/mailer_sending_job.rb @@ -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 diff --git a/app/jobs/users/trial_webhook_job.rb b/app/jobs/users/trial_webhook_job.rb new file mode 100644 index 00000000..d908d8c5 --- /dev/null +++ b/app/jobs/users/trial_webhook_job.rb @@ -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 diff --git a/app/mailers/users_mailer.rb b/app/mailers/users_mailer.rb new file mode 100644 index 00000000..111a4247 --- /dev/null +++ b/app/mailers/users_mailer.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 4c61d98e..36b1633f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/services/subscription/encode_jwt_token.rb b/app/services/subscription/encode_jwt_token.rb new file mode 100644 index 00000000..77c9e898 --- /dev/null +++ b/app/services/subscription/encode_jwt_token.rb @@ -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 diff --git a/app/views/devise/registrations/_api_key.html.erb b/app/views/devise/registrations/_api_key.html.erb index c04b7b85..a1230a2e 100644 --- a/app/views/devise/registrations/_api_key.html.erb +++ b/app/views/devise/registrations/_api_key.html.erb @@ -1,6 +1,14 @@

Use this API key to authenticate your requests.

<%= current_user.api_key %> + + <%# if ENV['QR_CODE_ENABLED'] == 'true' %> +

+ Or you can scan it in your Dawarich iOS app: + <%= api_key_qr_code(current_user) %> +

+ <%# end %> +

Docs: <%= link_to "API documentation", '/api-docs', class: 'underline hover:no-underline' %>

@@ -20,7 +28,6 @@
OR

Overland

<%= api_v1_overland_batches_url(api_key: current_user.api_key) %>

-

<%= 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' %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 5fb84f95..23be077a 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -4,7 +4,13 @@

Edit your account!

- <%= render 'devise/registrations/api_key' %> + <% if current_user.active? %> + <%= render 'devise/registrations/api_key' %> + <% else %> +

+ <%= 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. +

+ <% end %> <% if !DawarichSettings.self_hosted? %> <%= render 'devise/registrations/points_usage' %> <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e7b97017..1036f84d 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -31,5 +31,7 @@
<%= render SELF_HOSTED ? 'shared/footer' : 'shared/legal_footer' %>
+ + <%= render 'map/onboarding_modal' %> diff --git a/app/views/map/_onboarding_modal.html.erb b/app/views/map/_onboarding_modal.html.erb new file mode 100644 index 00000000..c6445fd6 --- /dev/null +++ b/app/views/map/_onboarding_modal.html.erb @@ -0,0 +1,16 @@ + + + diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 5140faf5..f29463b7 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -20,7 +20,9 @@ <% if user_signed_in? && current_user.can_subscribe? %> -
  • <%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %>
  • +
  • + <%= link_to 'Subscribe', "#{MANAGER_URL}/auth/dawarich?token=#{current_user.generate_subscription_token}", class: 'btn btn-sm btn-success' %> +
  • <% end %>
    @@ -70,9 +72,18 @@ - Continue Exploring + Continue Exploring

    You have 5 days left in your trial. Make the most of it!

    diff --git a/app/views/users_mailer/trial_expired.html.erb b/app/views/users_mailer/trial_expired.html.erb index a8f3663b..3294b88b 100644 --- a/app/views/users_mailer/trial_expired.html.erb +++ b/app/views/users_mailer/trial_expired.html.erb @@ -36,7 +36,7 @@
  • Enjoy beautiful interactive maps
  • - Subscribe to Continue + Subscribe to Continue

    Ready to unlock the full power of location insights? Subscribe now and pick up right where you left off!

    diff --git a/app/views/users_mailer/trial_expires_soon.html.erb b/app/views/users_mailer/trial_expires_soon.html.erb index 9f2d8992..c1e5ff6e 100644 --- a/app/views/users_mailer/trial_expires_soon.html.erb +++ b/app/views/users_mailer/trial_expires_soon.html.erb @@ -36,11 +36,11 @@
  • Visit detection and places management
  • - Subscribe Now + Subscribe Now

    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

    +

    Questions? Drop us a message at hi@dawarich.app or just reply to this email.

    Best regards,
    Evgenii from Dawarich

    diff --git a/app/views/users_mailer/welcome.html.erb b/app/views/users_mailer/welcome.html.erb index 2fb26167..07f80721 100644 --- a/app/views/users_mailer/welcome.html.erb +++ b/app/views/users_mailer/welcome.html.erb @@ -28,9 +28,9 @@
  • Export your data in various formats
  • - Start Exploring Dawarich + Start Exploring Dawarich -

    If you have any questions, feel free to drop us a message at hi@dawarich.app

    +

    If you have any questions, feel free to drop us a message at hi@dawarich.app or just reply to this email.

    Happy tracking!
    Evgenii from Dawarich

    diff --git a/app/views/users_mailer/welcome.text.erb b/app/views/users_mailer/welcome.text.erb index 41cff368..8cbf42d2 100644 --- a/app/views/users_mailer/welcome.text.erb +++ b/app/views/users_mailer/welcome.text.erb @@ -12,7 +12,7 @@ Your 7-day free trial has started. During this time, you can: Start exploring Dawarich: https://my.dawarich.app -If you have any questions, feel free to drop us a message at hi@dawarich.app +If you have any questions, feel free to drop us a message at hi@dawarich.app or just reply to this email. Happy tracking! Evgenii from Dawarich diff --git a/config/application.rb b/config/application.rb index 3d2dd0be..58530149 100644 --- a/config/application.rb +++ b/config/application.rb @@ -36,5 +36,7 @@ module Dawarich end config.active_job.queue_adapter = :sidekiq + + config.action_mailer.preview_paths << "#{Rails.root}/spec/mailers/previews" end end diff --git a/spec/mailers/previews/users_mailer_preview.rb b/spec/mailers/previews/users_mailer_preview.rb index e387692d..464549dc 100644 --- a/spec/mailers/previews/users_mailer_preview.rb +++ b/spec/mailers/previews/users_mailer_preview.rb @@ -1,9 +1,19 @@ -# Preview all emails at http://localhost:3000/rails/mailers/users_mailer -class UsersMailerPreview < ActionMailer::Preview +# frozen_string_literal: true - # Preview this email at http://localhost:3000/rails/mailers/users_mailer/welcome +class UsersMailerPreview < ActionMailer::Preview def welcome - UsersMailer.welcome + UsersMailer.with(user: User.last).welcome end + def explore_features + UsersMailer.with(user: User.last).explore_features + end + + def trial_expires_soon + UsersMailer.with(user: User.last).trial_expires_soon + end + + def trial_expired + UsersMailer.with(user: User.last).trial_expired + end end diff --git a/spec/mailers/users_mailer_spec.rb b/spec/mailers/users_mailer_spec.rb index f93bcd4e..844fcfd6 100644 --- a/spec/mailers/users_mailer_spec.rb +++ b/spec/mailers/users_mailer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails_helper" RSpec.describe UsersMailer, type: :mailer do diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index 88f06f02..50034082 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -3,16 +3,69 @@ require 'rails_helper' RSpec.describe Import, type: :model do + let(:user) { create(:user) } + subject(:import) { create(:import, user:) } + describe 'associations' do it { is_expected.to have_many(:points).dependent(:destroy) } - it { is_expected.to belong_to(:user) } + it 'belongs to a user' do + expect(user).to be_present + expect(import.user).to eq(user) + end end describe 'validations' do - subject { build(:import, name: 'test import') } - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name).scoped_to(:user_id) } + + it 'validates uniqueness of name scoped to user_id' do + create(:import, name: 'test_name', user: user) + + duplicate_import = build(:import, name: 'test_name', user: user) + expect(duplicate_import).not_to be_valid + expect(duplicate_import.errors[:name]).to include('has already been taken') + + other_user = create(:user) + different_user_import = build(:import, name: 'test_name', user: other_user) + expect(different_user_import).to be_valid + end + + describe 'file size validation' do + context 'when user is a trial user' do + let(:user) do + user = create(:user) + user.update!(status: :trial) + user + end + + it 'validates file size limit for large files' do + import = build(:import, user: user) + mock_file = double(attached?: true, blob: double(byte_size: 12.megabytes)) + allow(import).to receive(:file).and_return(mock_file) + + expect(import).not_to be_valid + expect(import.errors[:file]).to include('is too large. Trial users can only upload files up to 10MB.') + end + + it 'allows files under the size limit' do + import = build(:import, user: user) + mock_file = double(attached?: true, blob: double(byte_size: 5.megabytes)) + allow(import).to receive(:file).and_return(mock_file) + + expect(import).to be_valid + end + end + + context 'when user is a paid user' do + let(:user) { create(:user, status: :active) } + let(:import) { build(:import, user: user) } + + it 'does not validate file size limit' do + allow(import).to receive(:file).and_return(double(attached?: true, blob: double(byte_size: 12.megabytes))) + + expect(import).to be_valid + end + end + end end describe 'enums' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c38cc57a..dd7e69a9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -288,6 +288,8 @@ RSpec.describe User, type: :model do let(:user) { create(:user, status: :active, active_until: 1000.years.from_now) } it 'returns false' do + user.update(status: :active) + expect(user.can_subscribe?).to be_falsey end end @@ -304,6 +306,14 @@ RSpec.describe User, type: :model do expect(user.can_subscribe?).to be_truthy end end + + context 'when user is on trial' do + let(:user) { create(:user, :trial, active_until: 1.week.from_now) } + + it 'returns true' do + expect(user.can_subscribe?).to be_truthy + end + end end end diff --git a/spec/requests/imports_spec.rb b/spec/requests/imports_spec.rb index 0d1852de..56eb3333 100644 --- a/spec/requests/imports_spec.rb +++ b/spec/requests/imports_spec.rb @@ -203,6 +203,16 @@ RSpec.describe 'Imports', type: :request do expect(response).to have_http_status(200) end + + context 'when user is a trial user' do + let(:user) { create(:user, status: :trial) } + + it 'returns http success' do + get new_import_path + + expect(response).to have_http_status(200) + end + end end end From 7b46a663ce01962672135023bb75b5107d828a40 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 18 Aug 2025 20:53:32 +0200 Subject: [PATCH 07/12] Update changelog and app version --- .app_version | 2 +- CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.app_version b/.app_version index 473f1fb3..5c50d3ed 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.30.8 +0.30.9 diff --git a/CHANGELOG.md b/CHANGELOG.md index f89a0cf9..21ccee83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +# [0.30.9] - 2025-08-18 + +## Changed + +- Countries, visited during a trip, are now being calculated from points to improve performance. + + # [0.30.8] - 2025-08-01 ## Fixed From 464a183e8ffff8bafb7195bc8a2ac625dfe5d7b2 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 19 Aug 2025 18:29:34 +0200 Subject: [PATCH 08/12] Fix spec --- app/jobs/users/trial_webhook_job.rb | 1 + spec/jobs/users/trial_webhook_job_spec.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/jobs/users/trial_webhook_job.rb b/app/jobs/users/trial_webhook_job.rb index 6e29b462..512dd075 100644 --- a/app/jobs/users/trial_webhook_job.rb +++ b/app/jobs/users/trial_webhook_job.rb @@ -10,6 +10,7 @@ class Users::TrialWebhookJob < ApplicationJob user_id: user.id, email: user.email, active_until: user.active_until, + status: user.status, action: 'create_user' } diff --git a/spec/jobs/users/trial_webhook_job_spec.rb b/spec/jobs/users/trial_webhook_job_spec.rb index 63a65956..94a9e581 100644 --- a/spec/jobs/users/trial_webhook_job_spec.rb +++ b/spec/jobs/users/trial_webhook_job_spec.rb @@ -18,6 +18,8 @@ RSpec.describe Users::TrialWebhookJob, type: :job do expected_payload = { user_id: user.id, email: user.email, + active_until: user.active_until, + status: user.status, action: 'create_user' } From cdbd51c9f9b7c5e867ffbf235b9447a59a748442 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 19 Aug 2025 18:31:37 +0200 Subject: [PATCH 09/12] Update mailers --- app/mailers/users_mailer.rb | 8 ++++---- spec/mailers/users_mailer_spec.rb | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/mailers/users_mailer.rb b/app/mailers/users_mailer.rb index 111a4247..c7293a75 100644 --- a/app/mailers/users_mailer.rb +++ b/app/mailers/users_mailer.rb @@ -4,24 +4,24 @@ class UsersMailer < ApplicationMailer def welcome @user = params[:user] - mail(to: @user.email, subject: 'Welcome to Dawarich') + mail(to: @user.email, subject: 'Welcome to Dawarich!') end def explore_features @user = params[:user] - mail(to: @user.email, subject: 'Explore Dawarich features') + 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') + 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') + mail(to: @user.email, subject: 'πŸ’” Your Dawarich trial expired') end end diff --git a/spec/mailers/users_mailer_spec.rb b/spec/mailers/users_mailer_spec.rb index 844fcfd6..11789e2b 100644 --- a/spec/mailers/users_mailer_spec.rb +++ b/spec/mailers/users_mailer_spec.rb @@ -13,7 +13,7 @@ RSpec.describe UsersMailer, type: :mailer do let(:mail) { UsersMailer.with(user: user).welcome } it "renders the headers" do - expect(mail.subject).to eq("Welcome to Dawarich") + expect(mail.subject).to eq("Welcome to Dawarich!") expect(mail.to).to eq(["test@example.com"]) end @@ -26,7 +26,7 @@ RSpec.describe UsersMailer, type: :mailer do let(:mail) { UsersMailer.with(user: user).explore_features } it "renders the headers" do - expect(mail.subject).to eq("Explore Dawarich features") + expect(mail.subject).to eq("Explore Dawarich features!") expect(mail.to).to eq(["test@example.com"]) end end @@ -35,7 +35,7 @@ RSpec.describe UsersMailer, type: :mailer 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.subject).to eq("⚠️ Your Dawarich trial expires in 2 days") expect(mail.to).to eq(["test@example.com"]) end end @@ -44,7 +44,7 @@ RSpec.describe UsersMailer, type: :mailer 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.subject).to eq("πŸ’” Your Dawarich trial expired") expect(mail.to).to eq(["test@example.com"]) end end From b9764d39c3f1e8f3c4746fc611c4cc692cc67144 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 19 Aug 2025 18:55:22 +0200 Subject: [PATCH 10/12] Skip trial emails for active users --- app/jobs/users/mailer_sending_job.rb | 11 +++ app/models/import.rb | 20 ++--- app/models/user.rb | 2 +- spec/jobs/users/mailer_sending_job_spec.rb | 94 +++++++++++++++++++--- spec/models/user_spec.rb | 2 +- trials_feature_checklist.md | 28 ------- 6 files changed, 103 insertions(+), 54 deletions(-) delete mode 100644 trials_feature_checklist.md diff --git a/app/jobs/users/mailer_sending_job.rb b/app/jobs/users/mailer_sending_job.rb index 4b7db707..bbce993f 100644 --- a/app/jobs/users/mailer_sending_job.rb +++ b/app/jobs/users/mailer_sending_job.rb @@ -6,8 +6,19 @@ class Users::MailerSendingJob < ApplicationJob def perform(user_id, email_type, **options) user = User.find(user_id) + if trial_related_email?(email_type) && user.active? + Rails.logger.info "Skipping #{email_type} email for user #{user_id} - user is already subscribed" + return + end + params = { user: user }.merge(options) UsersMailer.with(params).public_send(email_type).deliver_later end + + private + + def trial_related_email?(email_type) + %w[trial_expires_soon trial_expired].include?(email_type.to_s) + end end diff --git a/app/models/import.rb b/app/models/import.rb index 08f1cfa5..74024798 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -23,18 +23,6 @@ class Import < ApplicationRecord user_data_archive: 8 } - private - - def file_size_within_limit - return unless file.attached? - - if file.blob.byte_size > 11.megabytes - errors.add(:file, 'is too large. Trial users can only upload files up to 10MB.') - end - end - - public - def process! if user_data_archive? process_user_data_archive! @@ -71,4 +59,12 @@ class Import < ApplicationRecord def remove_attached_file file.purge_later end + + def file_size_within_limit + return unless file.attached? + + if file.blob.byte_size > 11.megabytes + errors.add(:file, 'is too large. Trial users can only upload files up to 10MB.') + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index db156935..3f4046a0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,7 +28,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength attribute :admin, :boolean, default: false - enum :status, { inactive: 0, active: 1, trial: 3 } + enum :status, { inactive: 0, active: 1, trial: 2 } def safe_settings Users::SafeSettings.new(settings) diff --git a/spec/jobs/users/mailer_sending_job_spec.rb b/spec/jobs/users/mailer_sending_job_spec.rb index 9d0041a6..f4ce4faa 100644 --- a/spec/jobs/users/mailer_sending_job_spec.rb +++ b/spec/jobs/users/mailer_sending_job_spec.rb @@ -10,42 +10,88 @@ RSpec.describe Users::MailerSendingJob, type: :job do describe '#perform' do context 'when email_type is welcome' do - it 'sends welcome email' do + it 'sends welcome email to trial user' 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 + + it 'sends welcome email to active user' do + active_user = create(:user) + expect(UsersMailer).to receive(:with).with({ user: active_user }) + expect(UsersMailer).to receive(:welcome).and_return(mailer_double) + expect(mailer_double).to receive(:deliver_later) + + described_class.perform_now(active_user.id, 'welcome') + end end context 'when email_type is explore_features' do - it 'sends explore_features email' do + it 'sends explore_features email to trial user' 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 + + it 'sends explore_features email to active user' do + active_user = create(:user) + expect(UsersMailer).to receive(:with).with({ user: active_user }) + expect(UsersMailer).to receive(:explore_features).and_return(mailer_double) + expect(mailer_double).to receive(:deliver_later) + + described_class.perform_now(active_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) + context 'with trial user' 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') + described_class.perform_now(user.id, 'trial_expires_soon') + end + end + + context 'with active user' do + let(:active_user) { create(:user) } + + it 'skips sending trial_expires_soon email' do + expect(UsersMailer).not_to receive(:with) + expect(UsersMailer).not_to receive(:trial_expires_soon) + expect(Rails.logger).to receive(:info).with("Skipping trial_expires_soon email for user #{active_user.id} - user is already subscribed") + + described_class.perform_now(active_user.id, 'trial_expires_soon') + end 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) + context 'with trial user' 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') + described_class.perform_now(user.id, 'trial_expired') + end + end + + context 'with active user' do + let(:active_user) { create(:user) } + + it 'skips sending trial_expired email' do + expect(UsersMailer).not_to receive(:with) + expect(UsersMailer).not_to receive(:trial_expired) + expect(Rails.logger).to receive(:info).with("Skipping trial_expired email for user #{active_user.id} - user is already subscribed") + + described_class.perform_now(active_user.id, 'trial_expired') + end end end @@ -72,4 +118,28 @@ RSpec.describe Users::MailerSendingJob, type: :job do end end end + + describe '#trial_related_email?' do + subject { described_class.new } + + it 'returns true for trial_expires_soon' do + expect(subject.send(:trial_related_email?, 'trial_expires_soon')).to be true + end + + it 'returns true for trial_expired' do + expect(subject.send(:trial_related_email?, 'trial_expired')).to be true + end + + it 'returns false for welcome' do + expect(subject.send(:trial_related_email?, 'welcome')).to be false + end + + it 'returns false for explore_features' do + expect(subject.send(:trial_related_email?, 'explore_features')).to be false + end + + it 'returns false for unknown email types' do + expect(subject.send(:trial_related_email?, 'unknown_email')).to be false + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index dd7e69a9..2c07580a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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, trial: 3) } + it { is_expected.to define_enum_for(:status).with_values(inactive: 0, active: 1, trial: 2) } end describe 'callbacks' do diff --git a/trials_feature_checklist.md b/trials_feature_checklist.md deleted file mode 100644 index ed9da0b4..00000000 --- a/trials_feature_checklist.md +++ /dev/null @@ -1,28 +0,0 @@ -# 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 From a60e7f4124df8e8a4c3883c11daaa333008a58dd Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 19 Aug 2025 20:56:07 +0200 Subject: [PATCH 11/12] Fix mailer specs --- spec/jobs/users/mailer_sending_job_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/jobs/users/mailer_sending_job_spec.rb b/spec/jobs/users/mailer_sending_job_spec.rb index f4ce4faa..ba4b1de9 100644 --- a/spec/jobs/users/mailer_sending_job_spec.rb +++ b/spec/jobs/users/mailer_sending_job_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Users::MailerSendingJob, type: :job do before do allow(UsersMailer).to receive(:with).and_return(UsersMailer) + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) end describe '#perform' do @@ -59,12 +60,11 @@ RSpec.describe Users::MailerSendingJob, type: :job do end context 'with active user' do - let(:active_user) { create(:user) } + let(:active_user) { create(:user).tap { |u| u.update!(status: :active) } } it 'skips sending trial_expires_soon email' do expect(UsersMailer).not_to receive(:with) expect(UsersMailer).not_to receive(:trial_expires_soon) - expect(Rails.logger).to receive(:info).with("Skipping trial_expires_soon email for user #{active_user.id} - user is already subscribed") described_class.perform_now(active_user.id, 'trial_expires_soon') end @@ -83,12 +83,11 @@ RSpec.describe Users::MailerSendingJob, type: :job do end context 'with active user' do - let(:active_user) { create(:user) } + let(:active_user) { create(:user).tap { |u| u.update!(status: :active) } } it 'skips sending trial_expired email' do expect(UsersMailer).not_to receive(:with) expect(UsersMailer).not_to receive(:trial_expired) - expect(Rails.logger).to receive(:info).with("Skipping trial_expired email for user #{active_user.id} - user is already subscribed") described_class.perform_now(active_user.id, 'trial_expired') end From 4b55e1b29afcb2cf271fbaf5fec09419527c8f77 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 19 Aug 2025 21:27:34 +0200 Subject: [PATCH 12/12] Update Readme --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 789bd889..8ee904bf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🌍 Dawarich: Your Self-Hosted Location History Tracker +# 🌍 Dawarich: Your Self-Hostable Location History Tracker [![Discord](https://dcbadge.limes.pink/api/server/pHsBjpt5J8)](https://discord.gg/pHsBjpt5J8) | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/H2H3IDYDD) | [![Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dfreika%26type%3Dpatrons&style=for-the-badge)](https://www.patreon.com/freika) @@ -21,9 +21,14 @@ ## πŸ—ΊοΈ About Dawarich -**Dawarich** is a self-hosted web app designed to replace Google Timeline (aka Google Location History). It enables you to: +If you're looking for Dawarich Cloud, where everything is managed for you, check out [Dawarich Cloud](https://dawarich.app). + +**Dawarich** is a self-hostable web app designed to replace Google Timeline (aka Google Location History). +It enables you to: + +- Track your location history. - Visualize your data on an interactive map. -- Import your location history from Google Maps Timeline and Owntracks. +- Import your location history from Google Maps Timeline, OwnTracks, GPX, GeoJSON and some other sources - Explore statistics like the number of countries and cities visited, total distance traveled, and more! πŸ“„ **Changelog**: Find the latest updates [here](CHANGELOG.md). @@ -62,7 +67,7 @@ Simply install one of the supported apps on your device and configure it to send 1. Clone the repository. 2. Run the following command to start the app: ```bash - docker-compose up + docker-compose -f docker/docker-compose.yml up ``` 3. Access the app at `http://localhost:3000`.