linkio 1.0.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 +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE +21 -0
- data/README.md +295 -0
- data/app/controllers/linkio/application_controller.rb +50 -0
- data/app/controllers/linkio/deep_links_controller.rb +11 -0
- data/app/controllers/linkio/pending_links_controller.rb +33 -0
- data/app/controllers/linkio/referrals_controller.rb +23 -0
- data/app/controllers/linkio/well_known_controller.rb +16 -0
- data/config/routes.rb +18 -0
- data/lib/generators/linkio/install/install_generator.rb +29 -0
- data/lib/generators/linkio/install/templates/linkio.rb +63 -0
- data/lib/linkio/app_target.rb +79 -0
- data/lib/linkio/client.rb +213 -0
- data/lib/linkio/configuration.rb +152 -0
- data/lib/linkio/deep_link_data.rb +30 -0
- data/lib/linkio/engine.rb +24 -0
- data/lib/linkio/errors.rb +12 -0
- data/lib/linkio/pending_link_data.rb +57 -0
- data/lib/linkio/platform.rb +31 -0
- data/lib/linkio/rack/middleware.rb +105 -0
- data/lib/linkio/referral_data.rb +48 -0
- data/lib/linkio/request.rb +76 -0
- data/lib/linkio/result.rb +74 -0
- data/lib/linkio/smart_redirect.rb +135 -0
- data/lib/linkio/storage/base.rb +69 -0
- data/lib/linkio/storage/in_memory_storage.rb +79 -0
- data/lib/linkio/storage/redis_storage.rb +110 -0
- data/lib/linkio/utils.rb +125 -0
- data/lib/linkio/version.rb +5 -0
- data/lib/linkio.rb +71 -0
- metadata +136 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 64d4ce7d3188df34bc7eca2fe8a757d69549a5a259c58f0f0a62cc5b908a5e6b
|
|
4
|
+
data.tar.gz: 15560070030e575ba7a08bf68f1e8ee90f31de139118c24b19282a192c502555
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a61cffbb4f5f2a7a7f406a90c8f91e8c920c04be3d572c1f2c1050ef22e340f4771d0cf80cc6d0451b6cb8e87ca369d4228935909bce11be2524976cf4da9bc0
|
|
7
|
+
data.tar.gz: 8944ecce09f0a936c3623e6aa88cd28ecddeffcfb96f052d167eed4c5cda2af59fddbb31333e7381eee90ca9ec724a7a678572762b7759c94a62b281d24e82e9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Role-based multi-app routing.** Register multiple app targets with
|
|
13
|
+
`config.app "role" do |app| … end` and route on a configurable query/path
|
|
14
|
+
param (`config.role_param`, default `"role"`, with `config.default_role` as
|
|
15
|
+
the fallback). One link such as `/link?code=ABC&role=vendor` now opens the
|
|
16
|
+
matching app, falls back to that role's store on mobile, and redirects per
|
|
17
|
+
role on web.
|
|
18
|
+
- Per-target `fallback_url` with `{param}` templating for role-specific
|
|
19
|
+
redirects when the app is not installed (or for web visitors).
|
|
20
|
+
- `apple-app-site-association` and `assetlinks.json` now aggregate an entry for
|
|
21
|
+
every configured app target.
|
|
22
|
+
- `LinkIO::AppTarget` and `LinkIO::Utils.interpolate`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Single-app (flat) configuration is now the one-target special case of the
|
|
27
|
+
multi-app model and remains fully backward compatible.
|
|
28
|
+
|
|
29
|
+
## [1.0.0] - 2026-07-08
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Initial release — Ruby port of the Node.js LinkIO backend.
|
|
34
|
+
- Framework-agnostic `LinkIO::Client` with deep link handling, deferred deep
|
|
35
|
+
linking (IP fingerprint + device id), referral tracking, and generation of
|
|
36
|
+
`apple-app-site-association` and `assetlinks.json` documents.
|
|
37
|
+
- Smart redirect page that attempts to open the native app before falling back
|
|
38
|
+
to the App Store / Play Store.
|
|
39
|
+
- Pluggable storage: `LinkIO::Storage::InMemoryStorage`,
|
|
40
|
+
`LinkIO::Storage::RedisStorage`, and a `LinkIO::Storage::Base` interface for
|
|
41
|
+
custom adapters. Redis keys are namespaced identically to the Node.js backend.
|
|
42
|
+
- Mountable Rails engine (`LinkIO::Engine`) with controllers and routes, plus a
|
|
43
|
+
`linkio:install` generator.
|
|
44
|
+
- Rack middleware (`LinkIO::Rack::Middleware`) for non-Rails apps.
|
|
45
|
+
- Full RSpec test suite and RuboCop configuration.
|
|
46
|
+
|
|
47
|
+
[1.0.0]: https://github.com/pt-nakul-sharma/linkio-rails/releases/tag/v1.0.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nakul Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# LinkIO (Ruby)
|
|
2
|
+
|
|
3
|
+
**Self-hosted deep linking for mobile apps — an open-source alternative to Branch.io.**
|
|
4
|
+
|
|
5
|
+
LinkIO is the Ruby port of [LinkIO-Backend](https://github.com/pt-nakul-sharma/LinkIO-Backend) (Node.js). It gives you universal/app links, smart app-or-store redirects, fingerprint-based deferred deep linking, and referral tracking — with a framework-agnostic core, a mountable **Rails engine**, and **Rack middleware**.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/pt-nakul-sharma/linkio-rails/actions/workflows/ci.yml)
|
|
8
|
+
[](https://rubygems.org/gems/linkio)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Universal Links (iOS)** — automatic `apple-app-site-association` generation
|
|
16
|
+
- **App Links (Android)** — automatic `assetlinks.json` generation
|
|
17
|
+
- **Smart redirect** — tries to open the installed app, then falls back to the App Store / Play Store after a configurable timeout
|
|
18
|
+
- **Deferred deep linking** — IP-fingerprint matching preserves link data across the browser → install → app-open journey (works even when User-Agents differ)
|
|
19
|
+
- **Referral tracking** — record and query referrer → referee relationships
|
|
20
|
+
- **Role-based multi-app routing** — one link (`?code=…&role=…`) can open different apps (e.g. a User app vs a Vendor app) and fall back per role
|
|
21
|
+
- **Pluggable storage** — in-memory, Redis, or your own adapter
|
|
22
|
+
- **Three ways to mount** — plain Ruby, a Rails engine, or Rack middleware
|
|
23
|
+
- Fully tested with RSpec, zero required runtime dependencies
|
|
24
|
+
- **Runs on Ruby 3.0 through 3.4 and `head`**
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Gemfile
|
|
30
|
+
gem "linkio"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle install
|
|
35
|
+
# or
|
|
36
|
+
gem install linkio
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires Ruby >= 3.0 (tested on 3.0–3.4 and `head`).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick start (Rails)
|
|
44
|
+
|
|
45
|
+
LinkIO ships a mountable engine. Generate the initializer:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
rails generate linkio:install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Fill in your identifiers in `config/initializers/linkio.rb`:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
LinkIO.configure do |config|
|
|
55
|
+
config.domain = "yourdomain.com"
|
|
56
|
+
|
|
57
|
+
# iOS
|
|
58
|
+
config.ios_app_id = "123456789"
|
|
59
|
+
config.ios_team_id = "TEAMID123"
|
|
60
|
+
config.ios_bundle_id = "com.yourapp"
|
|
61
|
+
config.ios_app_scheme = "yourapp" # yourapp://
|
|
62
|
+
|
|
63
|
+
# Android
|
|
64
|
+
config.android_package_name = "com.yourapp"
|
|
65
|
+
config.android_app_scheme = "yourapp" # yourapp://
|
|
66
|
+
config.android_sha256_fingerprints = ["AA:BB:CC:...:99"]
|
|
67
|
+
|
|
68
|
+
config.fallback_timeout = 2500 # ms before falling back to the store
|
|
69
|
+
|
|
70
|
+
# Development. Use RedisStorage in production (see below).
|
|
71
|
+
config.storage = LinkIO::Storage::InMemoryStorage.new
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Mount the engine **at the domain root** (so the `/.well-known` files resolve):
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# config/routes.rb
|
|
79
|
+
mount LinkIO::Engine => "/"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That exposes:
|
|
83
|
+
|
|
84
|
+
| Method | Path | Purpose |
|
|
85
|
+
| ------ | ---- | ------- |
|
|
86
|
+
| GET | `/.well-known/apple-app-site-association` | iOS universal-link verification |
|
|
87
|
+
| GET | `/.well-known/assetlinks.json` | Android app-link verification |
|
|
88
|
+
| GET | `/link` | Generic deep link handler (any query params) |
|
|
89
|
+
| GET | `/pending-link` | Retrieve deferred link by client IP fingerprint |
|
|
90
|
+
| GET | `/pending-link/:device_id` | Retrieve deferred link by device id |
|
|
91
|
+
| POST | `/track-referral` | Record a referral (`referralCode`, `userId`, `metadata`) |
|
|
92
|
+
| GET | `/referrals/:referrer_id` | List referrals for a referrer |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Quick start (Rack / Sinatra / any Rack app)
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# config.ru
|
|
100
|
+
require "linkio"
|
|
101
|
+
require "redis"
|
|
102
|
+
|
|
103
|
+
LinkIO.configure do |config|
|
|
104
|
+
config.domain = "yourdomain.com"
|
|
105
|
+
config.ios_app_id = "123456789"
|
|
106
|
+
config.ios_team_id = "TEAMID123"
|
|
107
|
+
config.ios_bundle_id = "com.yourapp"
|
|
108
|
+
config.ios_app_scheme = "yourapp"
|
|
109
|
+
config.android_package_name = "com.yourapp"
|
|
110
|
+
config.android_app_scheme = "yourapp"
|
|
111
|
+
config.android_sha256_fingerprints = ["AA:BB:CC:...:99"]
|
|
112
|
+
config.storage = LinkIO::Storage::RedisStorage.new(Redis.new)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
use LinkIO::Rack::Middleware # uses LinkIO.client by default
|
|
116
|
+
run ->(_env) { [404, { "content-type" => "text/plain" }, ["Not found"]] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The middleware handles the LinkIO routes and passes everything else through to your app. Pass an explicit client with `use LinkIO::Rack::Middleware, client: my_client`.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Using the core directly
|
|
124
|
+
|
|
125
|
+
The engine and middleware are thin wrappers around `LinkIO::Client`, which you can use anywhere:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
client = LinkIO::Client.new(
|
|
129
|
+
domain: "yourdomain.com",
|
|
130
|
+
ios_app_id: "123456789",
|
|
131
|
+
ios_team_id: "TEAMID123",
|
|
132
|
+
ios_bundle_id: "com.yourapp",
|
|
133
|
+
ios_app_scheme: "yourapp",
|
|
134
|
+
android_package_name: "com.yourapp",
|
|
135
|
+
android_app_scheme: "yourapp",
|
|
136
|
+
android_sha256_fingerprints: ["AA:BB:CC:...:99"],
|
|
137
|
+
storage: LinkIO::Storage::InMemoryStorage.new
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Handle a deep link — returns a LinkIO::Result (redirect / html / json)
|
|
141
|
+
request = LinkIO::Request.from_rack(env) # or build one yourself
|
|
142
|
+
result = client.handle_deep_link(request)
|
|
143
|
+
result.redirect? # => true/false
|
|
144
|
+
result.location # => store URL (for redirects)
|
|
145
|
+
result.body # => HTML string or Ruby hash (json)
|
|
146
|
+
|
|
147
|
+
# Deferred deep linking (called by your mobile SDK after install)
|
|
148
|
+
client.pending_link_by_fingerprint(client_ip) # => LinkIO::DeepLinkData | nil
|
|
149
|
+
client.pending_link(device_id) # => LinkIO::DeepLinkData | nil
|
|
150
|
+
|
|
151
|
+
# Referrals
|
|
152
|
+
client.track_referral("CODE123", "user-42", { campaign: "summer" })
|
|
153
|
+
client.referrals("CODE123") # => [LinkIO::ReferralData]
|
|
154
|
+
client.referral_for_user("user-42") # => LinkIO::ReferralData | nil
|
|
155
|
+
|
|
156
|
+
# Verification documents
|
|
157
|
+
client.apple_app_site_association # => Hash
|
|
158
|
+
client.asset_links # => Array<Hash>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`LinkIO::Request` fields: `user_agent`, `full_url`, `query_params`, `path_params`, `client_ip`, `device_id`. Build one from a Rack env with `LinkIO::Request.from_rack(env)`, or construct it directly for full control.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Role-based routing (multiple apps)
|
|
166
|
+
|
|
167
|
+
Have more than one app — say a **User** app and a **Vendor** app — and want a single referral link to open the right one? Register an app target per role and LinkIO routes on a query/path param (default `role`):
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
LinkIO.configure do |config|
|
|
171
|
+
config.domain = "rokart.in"
|
|
172
|
+
config.role_param = "role" # which param selects the app (default: "role")
|
|
173
|
+
config.default_role = "user" # used when role is missing or unknown
|
|
174
|
+
|
|
175
|
+
config.app "user" do |app|
|
|
176
|
+
app.ios_app_id = "111111111"
|
|
177
|
+
app.ios_team_id = "TEAMID123"
|
|
178
|
+
app.ios_bundle_id = "com.rokart.user"
|
|
179
|
+
app.ios_app_scheme = "rokartuser"
|
|
180
|
+
app.android_package_name = "com.rokart.user"
|
|
181
|
+
app.android_app_scheme = "rokartuser"
|
|
182
|
+
app.android_sha256_fingerprints = ["AA:BB:...:99"]
|
|
183
|
+
# Optional: where to send visitors when the app isn't installed (per role).
|
|
184
|
+
app.fallback_url = "https://rokart.in/refer?code={code}&role=user"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
config.app "vendor" do |app|
|
|
188
|
+
app.ios_app_id = "222222222"
|
|
189
|
+
app.ios_team_id = "TEAMID123"
|
|
190
|
+
app.ios_bundle_id = "com.rokart.vendor"
|
|
191
|
+
app.ios_app_scheme = "rokartvendor"
|
|
192
|
+
app.android_package_name = "com.rokart.vendor"
|
|
193
|
+
app.android_app_scheme = "rokartvendor"
|
|
194
|
+
app.android_sha256_fingerprints = ["CC:DD:...:11"]
|
|
195
|
+
app.fallback_url = "https://rokart.in/refer?code={code}&role=vendor"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
config.storage = LinkIO::Storage::InMemoryStorage.new
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Now share `https://rokart.in/link?code=ABC123&role=vendor`:
|
|
203
|
+
|
|
204
|
+
| Visitor | Behaviour |
|
|
205
|
+
| ------- | --------- |
|
|
206
|
+
| iOS/Android with the **Vendor** app | Smart-redirect page opens `rokartvendor://link?code=ABC123&role=vendor` |
|
|
207
|
+
| Mobile **without** the app installed | Falls back to that role's `fallback_url` (or, if none set, that role's App Store / Play Store listing) |
|
|
208
|
+
| Desktop / web | Redirects to the role's `fallback_url` (or returns a JSON "open on mobile" message if none set) |
|
|
209
|
+
| `role` missing or unrecognised | Uses `default_role` (here, `user`) |
|
|
210
|
+
|
|
211
|
+
**Fallback URL templating.** `fallback_url` supports `{param}` placeholders filled (and URL-encoded) from the link's query — e.g. `https://rokart.in/refer?code={code}&role=vendor` becomes `…?code=ABC123&role=vendor`. Any query param can be interpolated, so this is fully generic — add `region`, `campaign`, etc. as needed.
|
|
212
|
+
|
|
213
|
+
**Verification files.** `apple-app-site-association` and `assetlinks.json` automatically include an entry for **every** registered app target, so both apps verify from the one domain.
|
|
214
|
+
|
|
215
|
+
The single-app (flat) configuration shown earlier is just the special case of one target — everything above still applies with `role` simply ignored.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Storage
|
|
220
|
+
|
|
221
|
+
Any object implementing the `LinkIO::Storage::Base` interface can back LinkIO.
|
|
222
|
+
|
|
223
|
+
### In-memory (development)
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
config.storage = LinkIO::Storage::InMemoryStorage.new
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Process-local and thread-safe. Data is lost on restart and not shared between workers — don't use it in production.
|
|
230
|
+
|
|
231
|
+
### Redis (production)
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
require "redis"
|
|
235
|
+
config.storage = LinkIO::Storage::RedisStorage.new(Redis.new)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Keys are namespaced (`linkio:pending:*`, `linkio:fingerprint:*`, `linkio:referral:*`, `linkio:referrer:*`) identically to the Node.js backend, so both implementations can share a Redis. Pending links are written with a TTL and expire automatically.
|
|
239
|
+
|
|
240
|
+
### Custom
|
|
241
|
+
|
|
242
|
+
Subclass `LinkIO::Storage::Base` and implement the nine methods (`save_pending_link`, `get_pending_link`, `delete_pending_link`, the three `*_by_fingerprint` variants, `save_referral`, `get_referrals_by_referrer`, `get_referral_by_referee`). Pending-link methods take/return `LinkIO::PendingLinkData`; referral methods take/return `LinkIO::ReferralData`. Both expose `#to_h` / `.from_h` for JSON-compatible (camelCase) serialization.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Configuration reference
|
|
247
|
+
|
|
248
|
+
**Top-level options:**
|
|
249
|
+
|
|
250
|
+
| Option | Required | Default | Description |
|
|
251
|
+
| ------ | -------- | ------- | ----------- |
|
|
252
|
+
| `domain` | ✅ | — | Your linking domain |
|
|
253
|
+
| `storage` | ✅ | — | A `LinkIO::Storage::Base` implementation |
|
|
254
|
+
| `role_param` | | `"role"` | Query/path param used to select an app target |
|
|
255
|
+
| `default_role` | | — | App target used when `role_param` is missing/unknown |
|
|
256
|
+
| `fallback_timeout` | | `2500` | ms before the redirect page falls back |
|
|
257
|
+
| `default_deep_link_path` | | — | Reserved for future use |
|
|
258
|
+
|
|
259
|
+
**Per-app-target options** (set directly on `config` for a single app, or inside `config.app "role" do |app| … end` for multiple):
|
|
260
|
+
|
|
261
|
+
| Option | Required | Default | Description |
|
|
262
|
+
| ------ | -------- | ------- | ----------- |
|
|
263
|
+
| `ios_app_id` | ✅ | — | Apple numeric app id (App Store URL) |
|
|
264
|
+
| `ios_team_id` | ✅ | — | Apple developer Team ID (AASA `appID`) |
|
|
265
|
+
| `ios_bundle_id` | ✅ | — | iOS bundle identifier (AASA `appID`) |
|
|
266
|
+
| `ios_app_scheme` | | — | Custom scheme, e.g. `yourapp` → `yourapp://` |
|
|
267
|
+
| `android_package_name` | ✅ | — | Android package name |
|
|
268
|
+
| `android_app_scheme` | | — | Custom scheme for Android |
|
|
269
|
+
| `android_sha256_fingerprints` | | `[]` | SHA-256 signing cert fingerprints for `assetlinks.json` |
|
|
270
|
+
| `fallback_url` | | — | Per-role destination when the app isn't installed / on web; supports `{param}` templating |
|
|
271
|
+
|
|
272
|
+
If `ios_app_scheme` / `android_app_scheme` is omitted, LinkIO redirects straight to the fallback (store or `fallback_url`) instead of rendering the smart-redirect page.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Ecosystem
|
|
277
|
+
|
|
278
|
+
LinkIO pairs with the platform SDKs: **LinkIO-iOS**, **LinkIO-Android**, and **LinkIO-React-Native**, plus the Node.js backend it is ported from.
|
|
279
|
+
|
|
280
|
+
## Development
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
bin/setup # bundle install
|
|
284
|
+
bundle exec rspec # run tests
|
|
285
|
+
bundle exec rubocop # lint
|
|
286
|
+
bundle exec rake # both
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Contributing
|
|
290
|
+
|
|
291
|
+
Bug reports and pull requests are welcome at <https://github.com/pt-nakul-sharma/linkio-rails>.
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
Released under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkIO
|
|
4
|
+
# Base controller for the engine. Provides access to the configured client
|
|
5
|
+
# and a helper to render a {LinkIO::Result}.
|
|
6
|
+
class ApplicationController < ActionController::Base
|
|
7
|
+
protect_from_forgery with: :null_session
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# @return [LinkIO::Client]
|
|
12
|
+
def linkio_client
|
|
13
|
+
LinkIO.client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Build a framework-agnostic {LinkIO::Request} from the Rails request.
|
|
17
|
+
#
|
|
18
|
+
# @return [LinkIO::Request]
|
|
19
|
+
def linkio_request
|
|
20
|
+
ip = LinkIO::Utils.client_ip(
|
|
21
|
+
forwarded_for: request.headers["X-Forwarded-For"],
|
|
22
|
+
ip: request.remote_ip,
|
|
23
|
+
remote_address: request.remote_addr
|
|
24
|
+
)
|
|
25
|
+
device_id = params[:deviceId] || request.headers["X-Device-Id"]
|
|
26
|
+
|
|
27
|
+
LinkIO::Request.new(
|
|
28
|
+
user_agent: request.user_agent,
|
|
29
|
+
full_url: request.original_url,
|
|
30
|
+
query_params: request.query_parameters,
|
|
31
|
+
path_params: request.path_parameters.except(:controller, :action),
|
|
32
|
+
client_ip: ip,
|
|
33
|
+
device_id: device_id
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Render a {LinkIO::Result} through the Rails response.
|
|
38
|
+
#
|
|
39
|
+
# @param result [LinkIO::Result]
|
|
40
|
+
def render_linkio(result)
|
|
41
|
+
if result.redirect?
|
|
42
|
+
redirect_to result.location, status: result.status, allow_other_host: true
|
|
43
|
+
elsif result.json?
|
|
44
|
+
render json: result.body, status: result.status
|
|
45
|
+
else
|
|
46
|
+
render html: result.body.html_safe, status: result.status, content_type: result.headers["content-type"]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkIO
|
|
4
|
+
# Handles incoming deep links (GET /link). Stores a pending link for deferred
|
|
5
|
+
# deep linking and redirects to the app or store based on platform.
|
|
6
|
+
class DeepLinksController < ApplicationController
|
|
7
|
+
def show
|
|
8
|
+
render_linkio(linkio_client.handle_deep_link(linkio_request))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkIO
|
|
4
|
+
# Retrieves pending links for deferred deep linking.
|
|
5
|
+
class PendingLinksController < ApplicationController
|
|
6
|
+
# GET /pending-link/:device_id
|
|
7
|
+
def by_device
|
|
8
|
+
deep_link = linkio_client.pending_link(params[:device_id])
|
|
9
|
+
respond_with_deep_link(deep_link)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# GET /pending-link (matched by client IP fingerprint)
|
|
13
|
+
def by_fingerprint
|
|
14
|
+
ip = LinkIO::Utils.client_ip(
|
|
15
|
+
forwarded_for: request.headers["X-Forwarded-For"],
|
|
16
|
+
ip: request.remote_ip,
|
|
17
|
+
remote_address: request.remote_addr
|
|
18
|
+
)
|
|
19
|
+
deep_link = linkio_client.pending_link_by_fingerprint(ip, request.user_agent)
|
|
20
|
+
respond_with_deep_link(deep_link)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def respond_with_deep_link(deep_link)
|
|
26
|
+
if deep_link.nil?
|
|
27
|
+
render json: { error: "No pending link found" }, status: :not_found
|
|
28
|
+
else
|
|
29
|
+
render json: deep_link.to_h
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkIO
|
|
4
|
+
# Tracks and lists referrals.
|
|
5
|
+
class ReferralsController < ApplicationController
|
|
6
|
+
# POST /track-referral
|
|
7
|
+
# Body: { referralCode:, userId:, metadata: }
|
|
8
|
+
def create
|
|
9
|
+
linkio_client.track_referral(
|
|
10
|
+
params[:referralCode],
|
|
11
|
+
params[:userId],
|
|
12
|
+
params[:metadata]&.to_unsafe_h
|
|
13
|
+
)
|
|
14
|
+
render json: { success: true }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# GET /referrals/:referrer_id
|
|
18
|
+
def index
|
|
19
|
+
referrals = linkio_client.referrals(params[:referrer_id])
|
|
20
|
+
render json: { referrals: referrals.map(&:to_h) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LinkIO
|
|
4
|
+
# Serves the domain verification files required by iOS and Android.
|
|
5
|
+
class WellKnownController < ApplicationController
|
|
6
|
+
# GET /.well-known/apple-app-site-association
|
|
7
|
+
def apple_app_site_association
|
|
8
|
+
render json: linkio_client.apple_app_site_association
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# GET /.well-known/assetlinks.json
|
|
12
|
+
def asset_links
|
|
13
|
+
render json: linkio_client.asset_links
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
LinkIO::Engine.routes.draw do
|
|
4
|
+
# Domain verification files (must be reachable at the domain root).
|
|
5
|
+
get "/.well-known/apple-app-site-association", to: "well_known#apple_app_site_association"
|
|
6
|
+
get "/.well-known/assetlinks.json", to: "well_known#asset_links"
|
|
7
|
+
|
|
8
|
+
# Generic deep link handler, e.g. /link?referralCode=ABC123
|
|
9
|
+
get "/link", to: "deep_links#show"
|
|
10
|
+
|
|
11
|
+
# Deferred deep linking.
|
|
12
|
+
get "/pending-link", to: "pending_links#by_fingerprint"
|
|
13
|
+
get "/pending-link/:device_id", to: "pending_links#by_device", constraints: { device_id: %r{[^/]+} }
|
|
14
|
+
|
|
15
|
+
# Referrals.
|
|
16
|
+
post "/track-referral", to: "referrals#create"
|
|
17
|
+
get "/referrals/:referrer_id", to: "referrals#index", constraints: { referrer_id: %r{[^/]+} }
|
|
18
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module LinkIO
|
|
6
|
+
module Generators
|
|
7
|
+
# Installs LinkIO into a Rails app: creates an initializer and prints the
|
|
8
|
+
# route to mount.
|
|
9
|
+
#
|
|
10
|
+
# rails generate linkio:install
|
|
11
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
def create_initializer
|
|
15
|
+
template "linkio.rb", "config/initializers/linkio.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show_readme
|
|
19
|
+
say ""
|
|
20
|
+
say "LinkIO installed. Next steps:", :green
|
|
21
|
+
say " 1. Fill in your app identifiers in config/initializers/linkio.rb"
|
|
22
|
+
say " 2. Mount the engine at the domain root in config/routes.rb:"
|
|
23
|
+
say ""
|
|
24
|
+
say " mount LinkIO::Engine => \"/\""
|
|
25
|
+
say ""
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# LinkIO configuration.
|
|
4
|
+
# Docs: https://github.com/pt-nakul-sharma/linkio-rails
|
|
5
|
+
LinkIO.configure do |config|
|
|
6
|
+
config.domain = "example.com"
|
|
7
|
+
|
|
8
|
+
# Wait this many ms in the redirect page before falling back to the store.
|
|
9
|
+
config.fallback_timeout = 2500
|
|
10
|
+
|
|
11
|
+
# --- Single app -----------------------------------------------------------
|
|
12
|
+
# iOS
|
|
13
|
+
config.ios_app_id = "123456789" # Apple app id (numeric)
|
|
14
|
+
config.ios_team_id = "TEAMID123" # Apple developer Team ID
|
|
15
|
+
config.ios_bundle_id = "com.example.app"
|
|
16
|
+
config.ios_app_scheme = "example" # custom URL scheme -> example://
|
|
17
|
+
|
|
18
|
+
# Android
|
|
19
|
+
config.android_package_name = "com.example.app"
|
|
20
|
+
config.android_app_scheme = "example" # custom URL scheme -> example://
|
|
21
|
+
config.android_sha256_fingerprints = [
|
|
22
|
+
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Optional per-role fallback used on web, and on mobile when the app is not
|
|
26
|
+
# installed. Supports {param} placeholders filled from the link's query.
|
|
27
|
+
# config.fallback_url = "https://example.com/download?code={code}"
|
|
28
|
+
|
|
29
|
+
# --- Multiple apps by role (e.g. a referral link ?code=ABC&role=vendor) ---
|
|
30
|
+
# Remove the single-app block above and register one target per role instead.
|
|
31
|
+
# The `role` query/path param selects which app to open.
|
|
32
|
+
#
|
|
33
|
+
# config.role_param = "role" # param used to route (default: "role")
|
|
34
|
+
# config.default_role = "user" # used when role is missing/unknown
|
|
35
|
+
#
|
|
36
|
+
# config.app "user" do |app|
|
|
37
|
+
# app.ios_app_id = "111111111"
|
|
38
|
+
# app.ios_team_id = "TEAMID123"
|
|
39
|
+
# app.ios_bundle_id = "com.example.user"
|
|
40
|
+
# app.ios_app_scheme = "exampleuser"
|
|
41
|
+
# app.android_package_name = "com.example.user"
|
|
42
|
+
# app.android_app_scheme = "exampleuser"
|
|
43
|
+
# app.android_sha256_fingerprints = ["AA:BB:...:99"]
|
|
44
|
+
# app.fallback_url = "https://example.com/refer?code={code}&role=user"
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# config.app "vendor" do |app|
|
|
48
|
+
# app.ios_app_id = "222222222"
|
|
49
|
+
# app.ios_team_id = "TEAMID123"
|
|
50
|
+
# app.ios_bundle_id = "com.example.vendor"
|
|
51
|
+
# app.ios_app_scheme = "examplevendor"
|
|
52
|
+
# app.android_package_name = "com.example.vendor"
|
|
53
|
+
# app.android_app_scheme = "examplevendor"
|
|
54
|
+
# app.android_sha256_fingerprints = ["CC:DD:...:11"]
|
|
55
|
+
# app.fallback_url = "https://example.com/refer?code={code}&role=vendor"
|
|
56
|
+
# end
|
|
57
|
+
|
|
58
|
+
# Storage. InMemoryStorage is for development only.
|
|
59
|
+
# For production use RedisStorage:
|
|
60
|
+
# require "redis"
|
|
61
|
+
# config.storage = LinkIO::Storage::RedisStorage.new(Redis.new)
|
|
62
|
+
config.storage = LinkIO::Storage::InMemoryStorage.new
|
|
63
|
+
end
|