From 6708e11ab3d7d989a203be6641c46e2653950f71 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Thu, 14 Aug 2025 20:50:22 +0200 Subject: [PATCH] Add limits for import size for trial users --- CHANGELOG.md | 4 +- app/controllers/stats_controller.rb | 4 +- .../controllers/direct_upload_controller.js | 19 +++++- .../onboarding_modal_controller.js | 42 +++++++++++++ app/jobs/users/trial_webhook_job.rb | 12 ++-- app/models/import.rb | 13 ++++ app/models/user.rb | 3 +- app/policies/import_policy.rb | 4 +- app/services/imports/create.rb | 5 ++ .../devise/registrations/_api_key.html.erb | 4 +- app/views/imports/_form.html.erb | 1 + app/views/map/_onboarding_modal.html.erb | 35 ++++++----- app/views/shared/_navbar.html.erb | 6 +- .../users_mailer/explore_features.html.erb | 2 +- app/views/users_mailer/trial_expired.html.erb | 2 +- .../users_mailer/trial_expires_soon.html.erb | 4 +- app/views/users_mailer/welcome.html.erb | 4 +- app/views/users_mailer/welcome.text.erb | 2 +- config/application.rb | 2 + spec/mailers/previews/users_mailer_preview.rb | 18 ++++-- spec/mailers/users_mailer_spec.rb | 2 + spec/models/import_spec.rb | 61 +++++++++++++++++-- spec/models/user_spec.rb | 10 +++ spec/requests/imports_spec.rb | 10 +++ 24 files changed, 221 insertions(+), 48 deletions(-) create mode 100644 app/javascript/controllers/onboarding_modal_controller.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f721b8c..eacd7c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +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-11 +# [0.30.9] - 2025-08-13 ## Added +- QR code for API key is implemented but hidden under feature flag until the iOS app supports it. - X-Dawarich-Response and X-Dawarich-Version headers are now returned for all API responses. +- Trial version for cloud users is now available. # [0.30.8] - 2025-08-01 diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 4bff870e..710f9b60 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -59,11 +59,11 @@ class StatsController < ApplicationController @stats.each do |year, stats| stats_by_month = stats.index_by(&:month) - + year_distances[year] = (1..12).map do |month| month_name = Date::MONTHNAMES[month] distance = stats_by_month[month]&.distance || 0 - + [month_name, distance] end end diff --git a/app/javascript/controllers/direct_upload_controller.js b/app/javascript/controllers/direct_upload_controller.js index 5be5b921..cc58436e 100644 --- a/app/javascript/controllers/direct_upload_controller.js +++ b/app/javascript/controllers/direct_upload_controller.js @@ -5,7 +5,8 @@ import { showFlashMessage } from "../maps/helpers" export default class extends Controller { static targets = ["input", "progress", "progressBar", "submit", "form"] static values = { - url: String + url: String, + userTrial: Boolean } connect() { @@ -50,6 +51,22 @@ export default class extends Controller { const files = this.inputTarget.files if (files.length === 0) return + // Check file size limits for trial users + if (this.userTrialValue) { + const MAX_FILE_SIZE = 11 * 1024 * 1024 // 11MB in bytes + const oversizedFiles = Array.from(files).filter(file => file.size > MAX_FILE_SIZE) + + if (oversizedFiles.length > 0) { + const fileNames = oversizedFiles.map(f => f.name).join(', ') + const message = `File size limit exceeded. Trial users can only upload files up to 10MB. Oversized files: ${fileNames}` + showFlashMessage('error', message) + + // Clear the file input + this.inputTarget.value = '' + return + } + } + console.log(`Uploading ${files.length} files`) this.isUploading = true diff --git a/app/javascript/controllers/onboarding_modal_controller.js b/app/javascript/controllers/onboarding_modal_controller.js new file mode 100644 index 00000000..5a20e1c2 --- /dev/null +++ b/app/javascript/controllers/onboarding_modal_controller.js @@ -0,0 +1,42 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["modal"] + static values = { showable: Boolean } + + connect() { + if (this.showableValue) { + // Listen for Turbo page load events to show modal after navigation completes + document.addEventListener('turbo:load', this.handleTurboLoad.bind(this)) + } + } + + disconnect() { + // Clean up event listener when controller is removed + document.removeEventListener('turbo:load', this.handleTurboLoad.bind(this)) + } + + handleTurboLoad() { + if (this.showableValue) { + this.checkAndShowModal() + } + } + + checkAndShowModal() { + const MODAL_STORAGE_KEY = 'dawarich_onboarding_shown' + const hasShownModal = localStorage.getItem(MODAL_STORAGE_KEY) + + if (!hasShownModal && this.hasModalTarget) { + // Show the modal + this.modalTarget.showModal() + + // Mark as shown in local storage + localStorage.setItem(MODAL_STORAGE_KEY, 'true') + + // Add event listener to handle when modal is closed + this.modalTarget.addEventListener('close', () => { + // Modal closed - state already saved + }) + } + } +} diff --git a/app/jobs/users/trial_webhook_job.rb b/app/jobs/users/trial_webhook_job.rb index d908d8c5..6e29b462 100644 --- a/app/jobs/users/trial_webhook_job.rb +++ b/app/jobs/users/trial_webhook_job.rb @@ -6,10 +6,14 @@ class Users::TrialWebhookJob < ApplicationJob 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 + payload = { + user_id: user.id, + email: user.email, + active_until: user.active_until, + action: 'create_user' + } + + token = Subscription::EncodeJwtToken.new(payload, ENV['JWT_SECRET_KEY']).call request_url = "#{ENV['MANAGER_URL']}/api/v1/users" headers = { diff --git a/app/models/import.rb b/app/models/import.rb index d22d5174..08f1cfa5 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -13,6 +13,7 @@ class Import < ApplicationRecord after_commit :remove_attached_file, on: :destroy validates :name, presence: true, uniqueness: { scope: :user_id } + validate :file_size_within_limit, if: -> { user.trial? } enum :status, { created: 0, processing: 1, completed: 2, failed: 3 } @@ -22,6 +23,18 @@ 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! diff --git a/app/models/user.rb b/app/models/user.rb index 36b1633f..db156935 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -98,7 +98,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength end def can_subscribe? - (active_until.nil? || active_until&.past?) && !DawarichSettings.self_hosted? + (trial? || !active_until&.future?) && !DawarichSettings.self_hosted? end def generate_subscription_token @@ -130,7 +130,6 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength end def activate - # TODO: Remove the `status` column in the future. update(status: :active, active_until: 1000.years.from_now) end diff --git a/app/policies/import_policy.rb b/app/policies/import_policy.rb index 0d1ceddf..fcaa2347 100644 --- a/app/policies/import_policy.rb +++ b/app/policies/import_policy.rb @@ -11,13 +11,13 @@ class ImportPolicy < ApplicationPolicy user.present? && record.user == user end - # Users can create new imports if they are active + # Users can create new imports if they are active or trial def new? create? end def create? - user.present? && user.active? + user.present? && (user.active? || user.trial?) end # Users can only edit their own imports diff --git a/app/services/imports/create.rb b/app/services/imports/create.rb index d86fe337..d7ad2323 100644 --- a/app/services/imports/create.rb +++ b/app/services/imports/create.rb @@ -56,7 +56,12 @@ class Imports::Create end def schedule_visit_suggesting(user_id, import) + return unless user.safe_settings.visits_suggestions_enabled? + points = import.points.order(:timestamp) + + return if points.none? + start_at = Time.zone.at(points.first.timestamp) end_at = Time.zone.at(points.last.timestamp) diff --git a/app/views/devise/registrations/_api_key.html.erb b/app/views/devise/registrations/_api_key.html.erb index a1230a2e..aeba5bfd 100644 --- a/app/views/devise/registrations/_api_key.html.erb +++ b/app/views/devise/registrations/_api_key.html.erb @@ -2,12 +2,12 @@

Use this API key to authenticate your requests.

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

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

- <%# end %> + <% end %>

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

diff --git a/app/views/imports/_form.html.erb b/app/views/imports/_form.html.erb index 35d2ec34..3f2857fb 100644 --- a/app/views/imports/_form.html.erb +++ b/app/views/imports/_form.html.erb @@ -1,6 +1,7 @@ <%= form_with model: import, class: "contents", data: { controller: "direct-upload", direct_upload_url_value: rails_direct_uploads_url, + direct_upload_user_trial_value: current_user.trial?, direct_upload_target: "form" } do |form| %>
diff --git a/app/views/map/_onboarding_modal.html.erb b/app/views/map/_onboarding_modal.html.erb index c6445fd6..c1d69b36 100644 --- a/app/views/map/_onboarding_modal.html.erb +++ b/app/views/map/_onboarding_modal.html.erb @@ -1,16 +1,21 @@ - - + +
+<% end %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index f29463b7..cf7ac463 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -72,8 +72,7 @@ - diff --git a/app/views/users_mailer/explore_features.html.erb b/app/views/users_mailer/explore_features.html.erb index f40ff0bc..9d8c64c0 100644 --- a/app/views/users_mailer/explore_features.html.erb +++ b/app/views/users_mailer/explore_features.html.erb @@ -43,7 +43,7 @@

Export your location data in multiple formats (GPX, GeoJSON) for backup or use with other applications.

- 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