Module: RubyNative

Defined in:
lib/ruby_native.rb,
lib/ruby_native/cli.rb,
lib/ruby_native/engine.rb,
lib/ruby_native/helper.rb,
lib/ruby_native/version.rb,
lib/ruby_native/cli/login.rb,
lib/ruby_native/iap/event.rb,
lib/ruby_native/cli/deploy.rb,
lib/ruby_native/cli/preview.rb,
lib/ruby_native/iap/decodable.rb,
lib/ruby_native/iap/verifiable.rb,
lib/ruby_native/native_version.rb,
lib/ruby_native/cli/credentials.rb,
lib/ruby_native/inertia_support.rb,
lib/ruby_native/iap/normalizable.rb,
lib/ruby_native/native_detection.rb,
lib/ruby_native/oauth_middleware.rb,
lib/ruby_native/set_cookie_header.rb,
lib/generators/ruby_native/iap_generator.rb,
lib/ruby_native/tunnel_cookie_middleware.rb,
app/models/ruby_native/iap/purchase_intent.rb,
lib/ruby_native/screenshots/sign_in_helper.rb,
app/controllers/ruby_native/aasa_controller.rb,
lib/ruby_native/iap/apple_webhook_processor.rb,
lib/generators/ruby_native/install_generator.rb,
app/controllers/ruby_native/config_controller.rb,
app/controllers/ruby_native/assetlinks_controller.rb,
app/controllers/ruby_native/auth/start_controller.rb,
app/controllers/ruby_native/iap/restores_controller.rb,
app/controllers/ruby_native/push/devices_controller.rb,
app/controllers/ruby_native/auth/sessions_controller.rb,
app/controllers/ruby_native/iap/purchases_controller.rb,
app/controllers/ruby_native/webhooks/apple_controller.rb,
app/controllers/ruby_native/iap/completions_controller.rb,
app/controllers/ruby_native/screenshots/sessions_controller.rb

Defined Under Namespace

Modules: Auth, Generators, Helper, IAP, InertiaSupport, NativeDetection, Push, Screenshots, SetCookieHeader, Webhooks Classes: AasaController, AssetlinksController, CLI, ConfigController, Engine, NativeVersion, OAuthMiddleware, TunnelCookieMiddleware

Constant Summary collapse

ERROR_SCREEN_STATES =

The native fallback screen has two states: offline (no connectivity) and generic (any other load failure). Each can carry a per-platform icon and localized copy.

%i[offline generic].freeze
ERROR_SCREEN_COPY_KEYS =
%i[title message].freeze
VERSION =
"0.15.0"

Class Method Summary collapse

Class Method Details

.backfill_error_iconsObject

Mirrors backfill_tab_icons for the error screen: fills a state's flat icon from its per-platform icons (ios first, then android), so the iOS app, which reads only the flat icon, still renders one. An explicit icon: wins.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ruby_native.rb', line 102

def self.backfill_error_icons
  errors = self.config[:errors]
  return unless errors.is_a?(Hash)

  ERROR_SCREEN_STATES.each do |state|
    state_config = errors[state]
    next unless state_config.is_a?(Hash)

    icons = state_config[:icons]
    next unless icons.is_a?(Hash)

    state_config[:icon] ||= icons[:ios] || icons[:android]
  end
end

.backfill_tab_iconsObject

Mirrors per-platform icons: into the legacy flat icon: field so native binaries that only read tab.icon keep rendering an icon. Explicit icon: wins; otherwise falls back to icons.ios, then icons.android.



87
88
89
90
91
92
93
94
95
96
# File 'lib/ruby_native.rb', line 87

def self.backfill_tab_icons
  Array(self.config[:tabs]).each do |tab|
    next unless tab.is_a?(Hash)

    icons = tab[:icons]
    next unless icons.is_a?(Hash)

    tab[:icon] ||= icons[:ios] || icons[:android]
  end
end

.config_as_jsonObject

The JSON served at GET /native/config. Identical to config, except the errors block is enriched: per-state icons from config/ruby_native.yml are merged with localized title/message pulled from the host app's I18n (ruby_native.errors.<state>.<key>), one entry per available locale. Only values the developer actually provided are emitted; the native apps fall back to bundled English copy for anything missing. Built on a deep copy so the in-memory config the server reads for view helpers is never mutated.



130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/ruby_native.rb', line 130

def self.config_as_json
  return config if config.nil?

  payload = config.deep_dup
  errors = error_screen_config(payload[:errors])
  if errors.empty?
    payload.delete(:errors)
  else
    payload[:errors] = errors
  end
  payload
end

.configure {|_self| ... } ⇒ Object

Yields:

  • (_self)

Yield Parameters:

  • _self (RubyNative)

    the object that the method was called on



32
33
34
# File 'lib/ruby_native.rb', line 32

def self.configure
  yield self
end

.error_screen_config(yaml_errors) ⇒ Object

Merges per-state error-screen icons (from YAML) with localized copy (from I18n) into the shape the native apps decode. Omits any state with neither an icon nor copy, so an untouched app sends no errors block at all.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/ruby_native.rb', line 146

def self.error_screen_config(yaml_errors)
  config = ERROR_SCREEN_STATES.each_with_object({}) do |state, result|
    entry = {}
    state_config = yaml_errors[state] if yaml_errors.is_a?(Hash)
    if state_config.is_a?(Hash)
      entry[:icon] = state_config[:icon] if state_config[:icon]
      entry[:icons] = state_config[:icons] if state_config[:icons]
    end
    ERROR_SCREEN_COPY_KEYS.each do |key|
      translations = error_screen_translations("#{state}.#{key}")
      entry[key] = translations unless translations.empty?
    end
    result[state] = entry unless entry.empty?
  end

  # The Retry button label is shared by both states, so it sits at the top of
  # the block rather than under a state.
  retry_label = error_screen_translations("retry")
  config[:retry] = retry_label unless retry_label.empty?
  config
end

.error_screen_translations(subkey) ⇒ Object

Reads ruby_native.errors.<subkey> for every available locale, keeping only the locales the developer actually translated. Copy lives in the host app's own locale files; the gem ships none.



171
172
173
174
175
176
# File 'lib/ruby_native.rb', line 171

def self.error_screen_translations(subkey)
  I18n.available_locales.each_with_object({}) do |locale, result|
    value = I18n.t("ruby_native.errors.#{subkey}", locale: locale, default: nil)
    result[locale] = value unless value.nil?
  end
end

.fire_subscription_callbacks(event) ⇒ Object



40
41
42
# File 'lib/ruby_native.rb', line 40

def self.fire_subscription_callbacks(event)
  subscription_callbacks.each { |cb| cb.call(event) }
end

.load_configObject



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/ruby_native.rb', line 44

def self.load_config
  path = Rails.root.join("config", "ruby_native.yml")
  return unless path.exist?

  parsed = YAML.load(render_config(path), filename: path.to_s, aliases: true)

  # An empty file, comments only, or ERB rendering to nothing parses to nil;
  # treat it like a missing file instead of crashing Rails boot.
  unless parsed.is_a?(Hash)
    Rails.logger.warn("[RubyNative] #{path} is empty or not a YAML mapping; ignoring it.")
    return
  end

  self.config = parsed.deep_symbolize_keys
  self.config[:app] ||= {}
  self.config[:app][:entry_path] ||= self.config.dig(:tabs, 0, :path) || "/"
  self.config[:auth] ||= {}
  normalize_oauth_paths
  normalize_linked_paths
  backfill_tab_icons
  backfill_error_icons
end

.normalize_linked_pathsObject

Entries are path prefixes: "/pair/" links every URL under it. A leading slash is added when missing, and a trailing "*" (an easy slip, since the AASA file uses one) is stripped so both platforms receive a plain prefix. Left absent when unconfigured so config.json doesn't grow an empty key.



207
208
209
210
211
212
213
214
# File 'lib/ruby_native.rb', line 207

def self.normalize_linked_paths
  return unless self.config.key?(:linked_paths)

  self.config[:linked_paths] = Array(self.config[:linked_paths])
    .map { |path| path.to_s.strip.sub(/\*+\z/, "") }
    .reject(&:empty?)
    .map { |path| path.start_with?("/") ? path : "/#{path}" }
end

.normalize_oauth_pathsObject

auth.oauth_paths must list only OAuth authorize paths, never their callbacks. The native app treats every listed path as a sign-in trigger and derives the provider from the last path segment, so a callback entry like "/auth/google/callback" would launch a bogus flow for a provider named "callback" and send sign-in into a loop. The callback round-trip is handled automatically by OAuthMiddleware's tracking cookie, so it never needs listing. Drop any entry that is the "/callback" child of another listed path and warn, so a copied-in callback can't break native sign-in. The value is always written back as an array: a scalar in YAML fails the whole native config decode on both platforms.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/ruby_native.rb', line 188

def self.normalize_oauth_paths
  auth = self.config[:auth]
  return unless auth.is_a?(Hash) && auth.key?(:oauth_paths)

  paths = Array(auth[:oauth_paths])
  callbacks = paths.select { |path| paths.any? { |start| path == "#{start}/callback" } }
  if callbacks.any?
    Rails.logger.warn(
      "[RubyNative] Ignoring OAuth callback path(s) in config/ruby_native.yml " \
      "(#{callbacks.join(", ")}). List only the authorize path; callbacks are handled automatically."
    )
  end
  auth[:oauth_paths] = paths - callbacks
end

.on_subscription_change(&block) ⇒ Object



36
37
38
# File 'lib/ruby_native.rb', line 36

def self.on_subscription_change(&block)
  subscription_callbacks << block
end

.render_config(path) ⇒ Object

config/ruby_native.yml is rendered as ERB before it is parsed, so a developer can interpolate Rails helpers into it. The motivating case is the navbar logo: logo: '<%= image_url("logo.png") %>' resolves to a fingerprinted asset URL the native app downloads and caches, and because the digest changes whenever the asset changes, the cache busts itself. A full URL (a CDN, say) works just as well; the app only ever sees a URL to fetch.

The template renders against the controller helper proxy, so image_url and friends behave exactly as they do in a view. With no request or asset host they degrade to a relative path -- asset helpers never raise "missing host" the way routing helpers do -- and the native app resolves any relative URL against the base URL it already fetched the config from.



79
80
81
82
# File 'lib/ruby_native.rb', line 79

def self.render_config(path)
  helpers = ActionController::Base.helpers
  ERB.new(path.read, trim_mode: "-").result(helpers.instance_eval { binding })
end