ruby_native 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5456e6607aa5961850b5a774484ca42467c6decfc6d605bef5cb53f302e652f
4
- data.tar.gz: 65cfc4e2c0bce516d97337ce74687591618f4225e05eac63a07c91086ce34c00
3
+ metadata.gz: 59fd793a678f35fc19666a9e5a182a3d33fceab98957ec486f4fb02983c8050c
4
+ data.tar.gz: 045fd16fede9352d7a2c86502b633b5f06f432a16a9cf2bcce8dae520601b522
5
5
  SHA512:
6
- metadata.gz: 4af1c21690a1af9976ba6fe925eaadca6787e75e6b2aab20a05b0f89fa049e854122a854e3b5b937d0f7fb8ef679420a23521c73c66cee9aa8fad06900e1b0ce
7
- data.tar.gz: b2b611b70b24476d9fd933d7631721d1000b94b694a3be31e415181d1e648e54e881662b8056ded977cd87fb5027dcbaa7399dfd1688099257ffe4349522bf56
6
+ metadata.gz: d4eae6af16060395f8a75d58fcd883b4cd69dc2ff69465d7da8be31cf1074d5c558733072ef68db069f721291a5734ad1f0c448c9b10f5576fd1c07223236680
7
+ data.tar.gz: be5da276be7b009b947090396f96dfd7db7e53b9d182ab3c94e1c837d8fbe46774196de0510266f0c4ee85c778d1bd77f833f204e2f94bd9609fff64b77e2331
data/README.md CHANGED
@@ -97,71 +97,11 @@ Place helpers in the `<body>`, not the `<head>`.
97
97
  - `native_push_tag` - requests push notification permission.
98
98
  - `native_back_button_tag(text = nil, **options)` - renders a back button for Normal Mode. Hidden by default, shown when the native app sets `body.can-go-back`. Not needed in [Advanced Mode](https://rubynative.com/docs/advanced-mode) where the system provides a native back button.
99
99
 
100
- ### Normal Mode
101
-
102
100
  - `native_form_tag` - marks the page as a form. The app skips form pages when navigating back.
103
-
104
- ### Advanced Mode
105
-
106
- These require the JavaScript setup described in [Advanced Mode setup](#advanced-mode-setup).
107
-
108
- - `native_form_data` - returns the data hash for the native form submit button. Pass to `form_with`'s `data:` option.
109
- - `native_submit_data` - returns the data hash for the native submit target. Pass to `form.submit`'s `data:` option.
110
- - `native_button_tag(title, url, ios_image:, side: :right, **options)` - adds a native navigation bar button.
111
- - `native_menu_tag(title:, side: :right, &block)` - displays a native action sheet menu.
112
- - `native_search_tag` - adds a native search bar.
113
-
114
- ## Advanced Mode setup
115
-
116
- 1. Install the JavaScript dependency:
117
-
118
- ```bash
119
- yarn add @hotwired/hotwire-native-bridge
120
- # or
121
- bin/importmap pin @hotwired/hotwire-native-bridge
122
- ```
123
-
124
- 2. Import the controllers in your JavaScript entrypoint:
125
-
126
- ```js
127
- import "ruby_native/bridge"
128
- ```
129
-
130
- ### `native_form_data` / `native_submit_data`
131
-
132
- ```erb
133
- <%= form_with model: @link, data: native_form_data do |f| %>
134
- <%= f.text_field :url %>
135
- <%= f.submit "Save", data: native_submit_data %>
136
- <% end %>
137
- ```
138
-
139
- ### `native_button_tag`
140
-
141
- ```erb
142
- <%= native_button_tag "Add a link", new_link_path, ios_image: "plus", class: "btn btn-primary" %>
143
- ```
144
-
145
- Options:
146
- - `ios_image:` - SF Symbol icon name (falls back to the title text)
147
- - `side:` - `:left` or `:right` (default). Left supplements the back button.
148
-
149
- ### `native_menu_tag`
150
-
151
- ```erb
152
- <%= native_menu_tag(title: "Actions") do |menu| %>
153
- <%= menu.item "Edit", edit_link_path(@link) %>
154
- <%= menu.item "Delete", link_path(@link), method: :delete, destructive: true %>
155
- <% end %>
156
- ```
157
-
158
- Options on `native_menu_tag`:
159
- - `title:` - action sheet title
160
- - `side:` - `:left` or `:right` (default)
161
-
162
- Options on `menu.item`:
163
- - `method:` - Turbo method (e.g., `:delete`)
164
- - `destructive: true` - red styling
101
+ - `native_navbar_tag(title, &block)` - native navigation bar with title, buttons, menus, and submit actions.
102
+ - `native_badge_tag(count, home:, tab:)` - sets the app icon and tab bar badge counts.
103
+ - `native_haptic_data(feedback = :success)` - returns a data hash that fires a haptic feedback on click.
104
+ - `native_overscroll_tag(top:, bottom:)` - per-page overscroll colors.
165
105
 
166
106
  ## Stylesheet
167
107
 
@@ -23,14 +23,3 @@
23
23
  body.can-go-back .native-back-button {
24
24
  display: inline;
25
25
  }
26
-
27
- [data-bridge-components~="form"]
28
- [data-controller~="bridge--form"]
29
- [type="submit"] {
30
- display: none;
31
- }
32
-
33
- [data-bridge-components~="button"]
34
- [data-controller~="bridge--button"] {
35
- display: none;
36
- }
data/config/importmap.rb CHANGED
@@ -1,3 +1 @@
1
- pin "ruby_native/bridge", to: "ruby_native/bridge/index.js"
2
- pin_all_from RubyNative::Engine.root.join("app/javascript/ruby_native/bridge"), under: "ruby_native/bridge"
3
1
  pin "ruby_native/back", to: "ruby_native/back.js"
@@ -46,11 +46,6 @@ module RubyNative
46
46
  say " 4. Preview on your device:"
47
47
  say " bundle exec ruby_native preview"
48
48
  say ""
49
- say " For Advanced Mode (native buttons, menus, search):"
50
- say " bin/importmap pin @hotwired/hotwire-native-bridge"
51
- say " Then add to your JavaScript entrypoint:"
52
- say " import \"ruby_native/bridge\""
53
- say ""
54
49
  if File.directory?(File.join(destination_root, ".claude"))
55
50
  say " Tip: .claude/ruby_native.md was added with setup instructions."
56
51
  say " Open Claude Code and ask \"what do I need to do next?\" for guided help."
@@ -21,9 +21,12 @@ rails generate ruby_native:install
21
21
  4. Add to your layout `<head>`:
22
22
 
23
23
  ```erb
24
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
24
25
  <%= stylesheet_link_tag :ruby_native %>
25
26
  ```
26
27
 
28
+ The `viewport-fit=cover` attribute is required so CSS `env(safe-area-inset-*)` variables return real values. If you already have a viewport meta tag, add `viewport-fit=cover` to its `content` attribute.
29
+
27
30
  5. Add to your layout `<body>`:
28
31
 
29
32
  ```erb
@@ -126,6 +129,35 @@ The gem auto-mounts at `/native`. No route configuration needed.
126
129
  - `GET /native/config` returns the YAML config as JSON
127
130
  - `POST /native/push/devices` registers a push notification device token
128
131
 
132
+ ## CLI
133
+
134
+ ### Deploy from CI
135
+
136
+ Use `--if-needed` to auto-deploy only when the gem version changes:
137
+
138
+ ```sh
139
+ bundle exec ruby_native deploy --if-needed
140
+ ```
141
+
142
+ Set `RUBY_NATIVE_TOKEN` as an environment variable for CI (no interactive login needed):
143
+
144
+ ```yaml
145
+ # GitHub Actions
146
+ - run: bundle exec ruby_native deploy --if-needed
147
+ env:
148
+ RUBY_NATIVE_TOKEN: ${{ secrets.RUBY_NATIVE_TOKEN }}
149
+ ```
150
+
151
+ ### Other commands
152
+
153
+ ```sh
154
+ bundle exec ruby_native login # authenticate (opens browser)
155
+ bundle exec ruby_native deploy # trigger a build
156
+ bundle exec ruby_native preview # start a tunnel and display a QR code
157
+ bundle exec ruby_native screenshots # capture App Store screenshots
158
+ bundle exec ruby_native logout # remove stored credentials
159
+ ```
160
+
129
161
  ## Common tasks
130
162
 
131
163
  ### Hide web navigation in the native app
@@ -7,6 +7,10 @@ module RubyNative
7
7
  PATH = File.join(Dir.home, ".ruby_native", "credentials")
8
8
 
9
9
  def self.token
10
+ ENV["RUBY_NATIVE_TOKEN"] || file_token
11
+ end
12
+
13
+ def self.file_token
10
14
  return unless File.exist?(PATH)
11
15
  JSON.parse(File.read(PATH))["token"]
12
16
  rescue JSON::ParserError
@@ -2,6 +2,7 @@ require "json"
2
2
  require "net/http"
3
3
  require "uri"
4
4
  require "ruby_native/cli/credentials"
5
+ require "ruby_native/version"
5
6
 
6
7
  module RubyNative
7
8
  class CLI
@@ -14,13 +15,22 @@ module RubyNative
14
15
  TokenExpiredError = Class.new(StandardError)
15
16
 
16
17
  def initialize(argv)
18
+ @if_needed = argv.include?("--if-needed")
17
19
  end
18
20
 
19
21
  def run
20
22
  load_config!
21
23
  ensure_authenticated!
22
24
  app_id = resolve_app_id!
25
+
26
+ if @if_needed && skip_build?(app_id)
27
+ puts "Ruby Native v#{RubyNative::VERSION} already built. Skipping deploy."
28
+ return
29
+ end
30
+
23
31
  build = trigger_build(app_id)
32
+ return if @if_needed
33
+
24
34
  poll_build_status(app_id, build)
25
35
  end
26
36
 
@@ -53,6 +63,39 @@ module RubyNative
53
63
  app_id
54
64
  end
55
65
 
66
+ # --- Version check ---
67
+
68
+ def skip_build?(app_id)
69
+ latest = fetch_latest_build(app_id)
70
+ return false unless latest
71
+
72
+ latest_gem_version = latest["gem_version"]
73
+ return false unless latest_gem_version
74
+
75
+ Gem::Version.new(latest_gem_version) >= Gem::Version.new(RubyNative::VERSION)
76
+ rescue ArgumentError
77
+ false
78
+ end
79
+
80
+ def fetch_latest_build(app_id)
81
+ uri = URI("#{HOST}/api/v1/apps/#{app_id}/builds/latest")
82
+ req = Net::HTTP::Get.new(uri)
83
+ req["Authorization"] = "Token #{Credentials.token}"
84
+
85
+ response = make_request(uri, req)
86
+
87
+ case response
88
+ when Net::HTTPSuccess
89
+ JSON.parse(response.body)
90
+ when Net::HTTPNoContent
91
+ nil
92
+ when Net::HTTPUnauthorized
93
+ raise TokenExpiredError
94
+ else
95
+ nil
96
+ end
97
+ end
98
+
56
99
  # --- Build ---
57
100
 
58
101
  def trigger_build(app_id)
@@ -62,6 +105,7 @@ module RubyNative
62
105
  req = Net::HTTP::Post.new(uri)
63
106
  req["Authorization"] = "Token #{Credentials.token}"
64
107
  req["Content-Type"] = "application/json"
108
+ req.body = JSON.generate(gem_version: RubyNative::VERSION)
65
109
 
66
110
  response = make_request(uri, req)
67
111
 
@@ -1,29 +1,16 @@
1
1
  module RubyNative
2
2
  module Helper
3
3
  def native_tabs_tag(enabled: true)
4
- safe_join([
5
- (tag.div(data: { native_tabs: true }, hidden: true) if enabled),
6
- tag.div(data: { controller: "bridge--tabs", bridge__tabs_enabled_value: enabled })
7
- ].compact)
4
+ return "".html_safe unless enabled
5
+ tag.div(data: { native_tabs: true }, hidden: true)
8
6
  end
9
7
 
10
8
  def native_form_tag
11
9
  tag.div(data: { native_form: true }, hidden: true)
12
10
  end
13
11
 
14
- def native_form_data(**data)
15
- merge_controller(data, "bridge--form")
16
- end
17
-
18
- def native_submit_data
19
- { bridge__form_target: "submit" }
20
- end
21
-
22
12
  def native_push_tag
23
- safe_join([
24
- tag.div(data: { native_push: true }, hidden: true),
25
- tag.div(data: { controller: "bridge--push" })
26
- ])
13
+ tag.div(data: { native_push: true }, hidden: true)
27
14
  end
28
15
 
29
16
  def native_back_button_tag(text = nil, **options)
@@ -35,53 +22,29 @@ module RubyNative
35
22
  tag.button(text || default_content, onclick: "RubyNative.postMessage({action: 'back'})", **options)
36
23
  end
37
24
 
38
- def native_search_tag
39
- tag.div(data: { controller: "bridge--search" })
40
- end
41
-
42
- def native_button_tag(title, url, ios_image: nil, side: :right, **options)
43
- data = options.delete(:data) || {}
44
- data[:controller] = "bridge--button"
45
- data[:bridge_side] = side.to_s
46
- data[:bridge_ios_image] = ios_image if ios_image
47
-
48
- link_to title, url, **options, data: data
49
- end
50
-
51
- def native_menu_tag(title:, side: :right, &block)
52
- builder = MenuBuilder.new(self)
53
- capture(builder, &block)
54
-
55
- tag.div(style: "display:none", data: {
56
- controller: "bridge--menu",
57
- bridge__menu_title_value: title,
58
- bridge__menu_side_value: side.to_s
59
- }) { builder.to_html }
60
- end
61
-
62
25
  def native_badge_tag(count = nil, home: nil, tab: nil)
63
26
  home = count if count && home.nil?
64
27
  tab = count if count && tab.nil?
65
28
 
66
- signal_data = { native_badge: "" }
67
- signal_data[:native_badge_home] = home unless home.nil?
68
- signal_data[:native_badge_tab] = tab unless tab.nil?
69
-
70
- bridge_data = { controller: "bridge--badge" }
71
- bridge_data[:bridge__badge_home_value] = home unless home.nil?
72
- bridge_data[:bridge__badge_tab_value] = tab unless tab.nil?
29
+ data = { native_badge: "" }
30
+ data[:native_badge_home] = home unless home.nil?
31
+ data[:native_badge_tab] = tab unless tab.nil?
73
32
 
74
- safe_join([
75
- tag.div(data: signal_data, hidden: true),
76
- tag.div(data: bridge_data)
77
- ])
33
+ tag.div(data: data, hidden: true)
78
34
  end
79
35
 
80
- def native_navbar_tag(title, &block)
36
+ def native_navbar_tag(title = nil, &block)
81
37
  builder = NavbarBuilder.new(self)
82
38
  capture(builder, &block) if block
83
39
 
84
- tag.div(data: { native_navbar: title }, hidden: true) { builder.to_html }
40
+ tag.div(data: { native_navbar: title.to_s }, hidden: true) { builder.to_html }
41
+ end
42
+
43
+ def native_fab_tag(icon:, href: nil, click: nil)
44
+ data = { native_fab: true, native_icon: icon }
45
+ data[:native_href] = href if href
46
+ data[:native_click] = click if click
47
+ tag.div(data: data, hidden: true)
85
48
  end
86
49
 
87
50
  def native_overscroll_tag(top:, bottom: nil)
@@ -91,49 +54,20 @@ module RubyNative
91
54
  def native_haptic_data(feedback = :success, **data)
92
55
  feedback = feedback.to_s
93
56
  feedback = "success" if feedback.empty?
94
-
95
57
  data[:native_haptic] = feedback
96
- data[:bridge__haptic_feedback_value] = feedback
97
- merge_controller(data, "bridge--haptic")
98
- end
99
-
100
- private
101
-
102
- def merge_controller(data, controller)
103
- data[:controller] = [data[:controller], controller].compact.join(" ")
104
58
  data
105
59
  end
106
60
 
107
- class MenuBuilder
108
- def initialize(context)
109
- @context = context
110
- @items = []
111
- end
112
-
113
- def item(title, url, method: nil, destructive: false, **options)
114
- data = options.delete(:data) || {}
115
- data[:bridge__menu_target] = "item"
116
- data[:turbo_method] = method if method
117
- data[:destructive] = "" if destructive
118
-
119
- @items << @context.link_to(title, url, **options, data: data, hidden: true)
120
- end
121
-
122
- def to_html
123
- @context.safe_join(@items)
124
- end
125
- end
126
-
127
61
  class NavbarBuilder
128
62
  def initialize(context)
129
63
  @context = context
130
64
  @items = []
131
65
  end
132
66
 
133
- def button(icon: nil, title: nil, href: nil, click: nil, position: :trailing, selected: false, &block)
67
+ def button(title = nil, icon: nil, href: nil, click: nil, position: :trailing, selected: false, &block)
134
68
  data = { native_button: "" }
135
- data[:native_icon] = icon if icon
136
69
  data[:native_title] = title if title
70
+ data[:native_icon] = icon if icon
137
71
  data[:native_href] = href if href
138
72
  data[:native_click] = click if click
139
73
  data[:native_position] = position.to_s
@@ -1,3 +1,3 @@
1
1
  module RubyNative
2
- VERSION = "0.6.0"
2
+ VERSION = "0.8.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_native
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Masilotti
@@ -72,15 +72,6 @@ files:
72
72
  - app/controllers/ruby_native/push/devices_controller.rb
73
73
  - app/controllers/ruby_native/webhooks/apple_controller.rb
74
74
  - app/javascript/ruby_native/back.js
75
- - app/javascript/ruby_native/bridge/badge_controller.js
76
- - app/javascript/ruby_native/bridge/button_controller.js
77
- - app/javascript/ruby_native/bridge/form_controller.js
78
- - app/javascript/ruby_native/bridge/haptic_controller.js
79
- - app/javascript/ruby_native/bridge/index.js
80
- - app/javascript/ruby_native/bridge/menu_controller.js
81
- - app/javascript/ruby_native/bridge/push_controller.js
82
- - app/javascript/ruby_native/bridge/search_controller.js
83
- - app/javascript/ruby_native/bridge/tabs_controller.js
84
75
  - app/models/ruby_native/iap/purchase_intent.rb
85
76
  - app/views/ruby_native/auth/start/show.html.erb
86
77
  - config/importmap.rb
@@ -1,21 +0,0 @@
1
- import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "badge"
5
- static values = { home: Number, tab: Number }
6
-
7
- connect() {
8
- super.connect()
9
- this.#update()
10
- }
11
-
12
- homeValueChanged() { this.#update() }
13
- tabValueChanged() { this.#update() }
14
-
15
- #update() {
16
- const data = {}
17
- if (this.hasHomeValue) data.home = this.homeValue
18
- if (this.hasTabValue) data.tab = this.tabValue
19
- this.send("connect", data)
20
- }
21
- }
@@ -1,30 +0,0 @@
1
- import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "button"
5
-
6
- connect() {
7
- super.connect()
8
- this.#addButton()
9
- }
10
-
11
- disconnect() {
12
- super.disconnect()
13
- this.#removeButton()
14
- }
15
-
16
- #addButton() {
17
- const element = this.bridgeElement
18
- const side = element.bridgeAttribute("side") || "right"
19
- const image = element.bridgeAttribute("ios-image")
20
- const data = { title: element.title, image }
21
-
22
- this.send(side, data, () => {
23
- this.element.click()
24
- })
25
- }
26
-
27
- #removeButton() {
28
- this.send("disconnect")
29
- }
30
- }
@@ -1,27 +0,0 @@
1
- import { BridgeComponent, BridgeElement } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "form"
5
- static targets = ["submit"]
6
-
7
- connect() {
8
- super.connect()
9
-
10
- const title = new BridgeElement(this.submitTarget).title
11
- this.send("connect", { submitTitle: title.trim() }, () => {
12
- this.submitTarget.click()
13
- })
14
-
15
- this.element.addEventListener("turbo:submit-start", this.submitStarted)
16
- this.element.addEventListener("turbo:submit-end", this.submitEnded)
17
- }
18
-
19
- disconnect() {
20
- super.disconnect()
21
- this.element.removeEventListener("turbo:submit-start", this.submitStarted)
22
- this.element.removeEventListener("turbo:submit-end", this.submitEnded)
23
- }
24
-
25
- submitStarted = () => this.send("submitDisabled")
26
- submitEnded = () => this.send("submitEnabled")
27
- }
@@ -1,11 +0,0 @@
1
- import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "haptic"
5
- static values = { feedback: { type: String, default: "success" } }
6
-
7
- vibrate() {
8
- const feedback = this.feedbackValue || "success"
9
- this.send("vibrate", { feedback })
10
- }
11
- }
@@ -1,18 +0,0 @@
1
- import { application } from "controllers/application"
2
- import TabsController from "ruby_native/bridge/tabs_controller"
3
- import FormController from "ruby_native/bridge/form_controller"
4
- import PushController from "ruby_native/bridge/push_controller"
5
- import MenuController from "ruby_native/bridge/menu_controller"
6
- import SearchController from "ruby_native/bridge/search_controller"
7
- import ButtonController from "ruby_native/bridge/button_controller"
8
- import HapticController from "ruby_native/bridge/haptic_controller"
9
- import BadgeController from "ruby_native/bridge/badge_controller"
10
-
11
- application.register("bridge--tabs", TabsController)
12
- application.register("bridge--form", FormController)
13
- application.register("bridge--push", PushController)
14
- application.register("bridge--menu", MenuController)
15
- application.register("bridge--search", SearchController)
16
- application.register("bridge--button", ButtonController)
17
- application.register("bridge--haptic", HapticController)
18
- application.register("bridge--badge", BadgeController)
@@ -1,22 +0,0 @@
1
- import { BridgeComponent, BridgeElement } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "menu"
5
- static targets = ["item"]
6
- static values = { title: String, side: { type: String, default: "right" } }
7
-
8
- connect() {
9
- super.connect()
10
-
11
- const items = this.itemTargets.map((el, index) => ({
12
- title: new BridgeElement(el).title,
13
- index,
14
- destructive: el.hasAttribute("data-destructive")
15
- }))
16
-
17
- this.send("connect", { title: this.titleValue, items, side: this.sideValue }, (message) => {
18
- const { selectedIndex } = message.data
19
- this.itemTargets[selectedIndex]?.click()
20
- })
21
- }
22
- }
@@ -1,10 +0,0 @@
1
- import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "push"
5
-
6
- connect() {
7
- super.connect()
8
- this.send("connect")
9
- }
10
- }
@@ -1,16 +0,0 @@
1
- import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "search"
5
-
6
- connect() {
7
- super.connect()
8
-
9
- this.send("connect", {}, (message) => {
10
- const query = message.data.query
11
- const detail = {query}
12
-
13
- this.dispatch("queried", {detail})
14
- })
15
- }
16
- }
@@ -1,11 +0,0 @@
1
- import { BridgeComponent } from "@hotwired/hotwire-native-bridge"
2
-
3
- export default class extends BridgeComponent {
4
- static component = "tabs"
5
- static values = { enabled: Boolean }
6
-
7
- connect() {
8
- super.connect()
9
- this.send("connect", { enabled: this.enabledValue })
10
- }
11
- }