Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- The password-reset page names the app in text (`display_name`) instead of showing `public/icon.svg`, the template's logo, which apps built from the template kept. The minimal and error page layouts' `<title>` use `display_name` too. The README lists the icons in `public/` as the template's to replace (#163)
- Mails name the app by the new `display_name` setting (`Native App Template`) instead of the identifier-like `NativeAppTemplate`: the sender name (`email.default_from_with_name`, also `support_with_name`), the invitation text, the name of a deleted inviter and the footer. The invitation mail leaves out the App Store / Google Play links while `mobile_app.app_store_url` / `play_store_url` are blank, which is now the default instead of placeholder URLs; the link labels are translated. `mobile_app.name`, `name_ios` and `name_android` are removed. The README's "App name and store links" covers both (#163)
- API change: add an admin-set full feature access override for the paid clients. `shopkeepers.feature_access_state` (`normal_feature_access` by default, or `all_feature_access`, indexed) is editable only in `/madmin`; sign-up and `PATCH /shopkeeper_auth` ignore it. Sign-in returns `feature_access_state` and `account_owner_feature_access_state`, and account responses return `owner_feature_access_state`, so the apps can unlock every paid feature in an account whose owner has the override (testing on production, staff/partner/comp accounts, App Review demo accounts). The README's "Full feature access override" covers it and RevenueCat promotional entitlements as the no-code alternative. Ported from myturntagapi
- Fix saving a shopkeeper in madmin failing with 500 (`PG::UniqueViolation` on `index_shopkeepers_on_reset_password_token` or `index_shopkeepers_on_confirmation_token`). The edit form sends empty token fields as `""`, so a second shopkeeper with no reset or confirmation token collided with the first. madmin's shopkeeper form no longer shows `reset_password_token` and `confirmation_token` (read only on the show page), and a migration clears the `""` already stored. An earlier version of this fix normalized blank tokens to NULL in `Shopkeeper`; that also rewrote finder values, so `where(reset_password_token: "")` matched every shopkeeper without a token, and it was removed. Ported from myturntagapi, where it hit production (#158)
- API change: `Shop#time_zone` and `Shopkeeper#time_zone` must be a Rails time zone name (`ActiveSupport::TimeZone.all.map(&:name)`, e.g. `Tokyo`, `London`); anything else, IANA ids such as `Asia/Tokyo` included, gets 422 "Time zone is not included in the list" from `PATCH /shops/:id`, `POST /shops`, sign-up and `PATCH /shopkeeper_auth` instead of being stored. `test/config/time_zones_test.rb` pins the list against `test/fixtures/files/rails_time_zones.tsv`, which the iOS and Android apps embed for their time zone pickers (#155)
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ bin/setup

Push notifications (used by the paid iOS/Android clients) need APNs and FCM credentials. Add them per environment with `bin/rails credentials:edit --environment <env>` under `action_push_native:apns` and `action_push_native:fcm`; see `config/push.yml` for the expected keys.

## App name and store links

`config/settings.yml` has two kinds of app name. `display_name` (`Native App Template`) is the name people read: mails (sender name, invitation text, footer) and the HTML pages use it. `site.name` and `business.name` (`NativeAppTemplate`) are the one-word identifier-like names; keep them for code that needs one.

The invitation mail links the App Store and Google Play only when `mobile_app.app_store_url` / `mobile_app.play_store_url` are set. Both are blank by default, so no placeholder link is sent; fill them in once the apps are published.

The icons in `public/` (`favicon.ico`, `icon.svg`, `icon.png`, `icon-192.png`, `icon-512.png`, `apple-touch-icon.png`) are the template's and only appear as browser favicons; replace them with your app's icons. The HTML pages (password reset, email confirmation, error pages) name the app in text with `display_name` instead of showing a logo.

## Running NativeAppTemplate API on your Wi-Fi

Copy `.env.sample` to `.env` and set `HOST` to your current Wi-Fi IP. On macOS: `ipconfig getifaddr en0`. `bin/dev` binds Rails to that address so the dev server is reachable from both the host browser and from any phone on the same network at `http://<wifi-ip>:3000`. When your Wi-Fi IP changes, update `HOST` here and the matching `NATIVEAPPTEMPLATE_API_DOMAIN` in the mobile apps (Xcode scheme for iOS, `~/.gradle/gradle.properties` for Android) — Rails fails loudly if `HOST` is unset, which keeps the three sides honest. Never use `127.0.0.1`, `localhost`, or `0.0.0.0`.
Expand Down
15 changes: 14 additions & 1 deletion app/mailers/shopkeeper/notification_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ def invited
@account = @accounts_invitation.account
# invited_by is nullified when the inviter is deleted; the invitation can
# still be resent, so name the app instead
@inviter_name = @accounts_invitation.invited_by&.name || ConfigSettings.mobile_app.name
@inviter_name = @accounts_invitation.invited_by&.name || ConfigSettings.display_name
@store_links = store_links

name = @accounts_invitation.name
email = @accounts_invitation.email
Expand Down Expand Up @@ -45,4 +46,16 @@ def reset_password_instructions
subject: I18n.t("shopkeeper.notification_mailer.reset_password_instructions.subject")
)
end

private

# Blank store URLs (the default in config/settings.yml) are left out, so
# the mail never links a placeholder before the apps are published
def store_links
app = ConfigSettings.display_name
{
I18n.t("shopkeeper.notification_mailer.invited.ios_app", mobile_app: app) => ConfigSettings.mobile_app.app_store_url,
I18n.t("shopkeeper.notification_mailer.invited.android_app", mobile_app: app) => ConfigSettings.mobile_app.play_store_url
}.select { |_label, url| url.present? }
end
end
2 changes: 1 addition & 1 deletion app/views/layouts/errors.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<% if content_for?(:title) %>
<%= yield :title %> |
<% end %>
<%= ConfigSettings.site.name %>
<%= ConfigSettings.display_name %>
</title>

<%= csrf_meta_tags %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/layouts/mailer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
<tr>
<td valign="top" class="addressContent">
<div>
<em><%= t(".copyright_html", year: Date.current.year, business_name: ConfigSettings.business.name) %></em>
<em><%= t(".copyright_html", year: Date.current.year, name: ConfigSettings.display_name) %></em>
</div>
</td>
</tr>
Expand All @@ -126,7 +126,7 @@
<td valign="middle" class="bodyContent">
<div>
&nbsp;
<%= link_to ConfigSettings.site.name, ConfigSettings.site.url, target: :_blank %>
<%= link_to ConfigSettings.display_name, ConfigSettings.site.url, target: :_blank %>
&nbsp;
</div>
</td>
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/mailer.text.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<%= yield %>

<%= ConfigSettings.site.name %>: <%= ConfigSettings.site.url %>
<%= ConfigSettings.display_name %>: <%= ConfigSettings.site.url %>
2 changes: 1 addition & 1 deletion app/views/layouts/minimal.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<% if content_for?(:title) %>
<%= yield :title %> |
<% end %>
<%= ConfigSettings.site.name %>
<%= ConfigSettings.display_name %>
</title>

<%= csrf_meta_tags %>
Expand Down
9 changes: 5 additions & 4 deletions app/views/shopkeeper/notification_mailer/invited.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<p style="margin-bottom:2rem;">
<%= t(".you_can_accept_or_decline") %>
<br>
<%= t(".enter_your_invitation_code_in_the_mobile_app_html", mobile_app: ConfigSettings.mobile_app.name) %>
<%= t(".enter_your_invitation_code_in_the_mobile_app_html", mobile_app: ConfigSettings.display_name) %>
</p>

<p><%= t(".instructions_heading") %></p>
<p>1. <%= t(".open_mobile_app_html", mobile_app: ConfigSettings.mobile_app.name) %></p>
<p>1. <%= t(".open_mobile_app_html", mobile_app: ConfigSettings.display_name) %></p>
<p>2. <%= t(".create_account_or_sign_in_with_html", email: @accounts_invitation.email) %></p>
<p>3. <%= t(".select_organization_tab") %></p>
<p>4. <%= t(".tap_join_button") %></p>
Expand All @@ -20,7 +20,8 @@
<strong><%= @accounts_invitation.token %></strong>
</h2>

<p><%= link_to ConfigSettings.mobile_app.name_ios, ConfigSettings.mobile_app.app_store_url, target: :_blank %></p>
<p><%= link_to ConfigSettings.mobile_app.name_android, ConfigSettings.mobile_app.play_store_url, target: :_blank %></p>
<% @store_links.each do |label, url| %>
<p><%= link_to label, url, target: :_blank %></p>
<% end %>

<p><%= t(".expires_in", expires_in: ConfigSettings.accounts_invitation.expires_in_hours) %></p>
11 changes: 7 additions & 4 deletions app/views/shopkeeper/notification_mailer/invited.text.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

<%= t(".you_can_accept_or_decline") %>

<%= strip_tags t(".enter_your_invitation_code_in_the_mobile_app_html", mobile_app: ConfigSettings.mobile_app.name) %>
<%= strip_tags t(".enter_your_invitation_code_in_the_mobile_app_html", mobile_app: ConfigSettings.display_name) %>

<%= t(".instructions_heading") %>
1. <%= strip_tags t(".open_mobile_app_html", mobile_app: ConfigSettings.mobile_app.name) %>
1. <%= strip_tags t(".open_mobile_app_html", mobile_app: ConfigSettings.display_name) %>
2. <%= strip_tags t(".create_account_or_sign_in_with_html", email: @accounts_invitation.email) %>
3. <%= t(".select_organization_tab") %>
4. <%= t(".tap_join_button") %>
Expand All @@ -14,7 +14,10 @@

<%= @accounts_invitation.token %>

<%= ConfigSettings.mobile_app.name_ios %>: <%= ConfigSettings.mobile_app.app_store_url %>
<%= ConfigSettings.mobile_app.name_android %>: <%= ConfigSettings.mobile_app.play_store_url %>
<% if @store_links.any? %>
<% @store_links.each do |label, url| %>
<%= label %>: <%= url %>
<% end %>

<% end %>
<%= t(".expires_in", expires_in: ConfigSettings.accounts_invitation.expires_in_hours) %>
2 changes: 1 addition & 1 deletion app/views/shopkeeper_auth/reset_passwords/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<%= image_tag "/icon.svg?color=sky&shade=600", class: "mx-auto h-10 w-auto", alt: ConfigSettings.business.name %>
<p class="text-center text-2xl font-semibold leading-10 tracking-tight text-sky-600"><%= ConfigSettings.display_name %></p>
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"><%= t("change_your_password") %></h2>
</div>

Expand Down
4 changes: 3 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ en:
tap_join_button: "Tap Join button."
enter_your_invitation_code: "Enter your invitation code below."
if_you_accepted_invitation_switch_to_organization: "If you accepted the invitation, switch to the organization(Organizations > [Organization] > Switch)."
ios_app: "%{mobile_app} for iOS"
android_app: "%{mobile_app} for Android"
expires_in: "This invitation will expire in %{expires_in} hours."
reset_password_instructions:
subject: "Reset password instructions"
Expand Down Expand Up @@ -123,7 +125,7 @@ en:

layouts:
mailer:
copyright_html: "&copy; %{year} %{business_name} All rights reserved."
copyright_html: "&copy; %{year} %{name} All rights reserved."

activerecord:
attributes:
Expand Down
4 changes: 3 additions & 1 deletion config/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ ja:
tap_join_button: "「参加」ボタンをタップします。"
enter_your_invitation_code: "下記の招待コードを入力します。"
if_you_accepted_invitation_switch_to_organization: "招待を承認したら、その組織に切り替えます(組織 > [組織名] > 切り替え)。"
ios_app: "%{mobile_app} iOS版"
android_app: "%{mobile_app} Android版"
expires_in: "この招待の有効期限は%{expires_in}時間です。"
reset_password_instructions:
subject: "パスワード再設定のご案内"
Expand Down Expand Up @@ -114,7 +116,7 @@ ja:

layouts:
mailer:
copyright_html: "&copy; %{year} %{business_name} All rights reserved."
copyright_html: "&copy; %{year} %{name} All rights reserved."

activerecord:
models:
Expand Down
20 changes: 13 additions & 7 deletions config/settings.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# The app's name as people read it, in mails and on the HTML pages. The
# identifier-like names below (site.name, business.name) stay as they are
# for code that needs a one-word name.
display_name: Native App Template

site:
name: NativeAppTemplate
domain: nativeapptemplate.com
Expand All @@ -8,19 +13,20 @@ business:

email:
support: support@nativeapptemplate.com
support_with_name: NativeAppTemplate <support@nativeapptemplate.com>
support_with_name: Native App Template <support@nativeapptemplate.com>
default_from: noreply@nativeapptemplate.com
default_from_with_name: NativeAppTemplate <noreply@nativeapptemplate.com>
default_from_with_name: Native App Template <noreply@nativeapptemplate.com>

app:
domain: api.nativeapptemplate.com

# Store links in the invitation mail. Each is left out of the mail while
# blank; set them once the apps are published, e.g.
# app_store_url: https://apps.apple.com/app/<name>/id<number>
# play_store_url: https://play.google.com/store/apps/details?id=<package>
mobile_app:
name: NativeAppTemplate
name_ios: NativeAppTemplate for iOS
name_android: NativeAppTemplate for Android
app_store_url: https://apps.apple.com/app/nativeapptemplate/idxxxxxxxxxx
play_store_url: https://play.google.com/store/apps/details?id=com.yourdomain.nativeapptemplate
app_store_url: ""
play_store_url: ""

minimum_password_length: 8

Expand Down
6 changes: 6 additions & 0 deletions test/controllers/errors_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ class ErrorsControllerTest < ActionController::TestCase
assert_match(/text\/html/, response.media_type)
end

test "HTML error page title names the app by its display name" do
get :not_found

assert_select "title", text: /#{Regexp.escape(ConfigSettings.display_name)}/
end

test "internal_server_error responds with 500 JSON when content-type is JSON" do
@request.headers["Content-Type"] = "application/json"
get :internal_server_error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,15 @@ class ShopkeeperAuth::ResetPasswordsControllerTest < ActionDispatch::Integration
assert_response :success
assert_match "tok-xyz", response.body
end

# The page showed public/icon.svg, the template's logo, which an app built
# from the template kept. It names the app in text instead.
test "edit names the app in text instead of showing the template logo" do
get edit_shopkeeper_auth_reset_password_url(reset_password_token: "abc123")

assert_response :success
assert_select "p", text: ConfigSettings.display_name
assert_select "img", count: 0
assert_select "title", text: /#{Regexp.escape(ConfigSettings.display_name)}/
end
end
Loading
Loading