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/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, 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.14.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.



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

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.



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

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.



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

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



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

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.



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

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.



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

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



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

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

.load_configObject



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

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.



201
202
203
204
205
206
207
208
# File 'lib/ruby_native.rb', line 201

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.



185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ruby_native.rb', line 185

def self.normalize_oauth_paths
  paths = Array(self.config.dig(:auth, :oauth_paths))
  callbacks = paths.select { |path| paths.any? { |start| path == "#{start}/callback" } }
  return if callbacks.empty?

  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."
  )
  self.config[:auth][:oauth_paths] = paths - callbacks
end

.on_subscription_change(&block) ⇒ Object



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

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.



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

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