@instaread-co/react-native-instaread-sdk 1.3.4

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/InstareadReactNative.podspec +29 -0
  3. package/LICENSE +9 -0
  4. package/README.md +146 -0
  5. package/android/build.gradle +105 -0
  6. package/android/libs/instaread-sdk.aar +0 -0
  7. package/android/libs/instaread-sdk.version +1 -0
  8. package/android/src/main/AndroidManifest.xml +6 -0
  9. package/android/src/main/java/com/instaread/reactnative/InstareadNativePlayerViewManagerBase.kt +38 -0
  10. package/android/src/main/java/com/instaread/reactnative/InstareadPlayerHostView.kt +133 -0
  11. package/android/src/main/java/com/instaread/reactnative/InstareadReactNativeModule.kt +35 -0
  12. package/android/src/main/java/com/instaread/reactnative/InstareadReactNativePackage.kt +20 -0
  13. package/android/src/newarch/java/com/instaread/reactnative/InstareadNativePlayerViewManager.kt +33 -0
  14. package/android/src/oldarch/java/com/instaread/reactnative/InstareadNativePlayerViewManager.kt +29 -0
  15. package/ios/InstareadNativePlayerComponentView.h +17 -0
  16. package/ios/InstareadNativePlayerComponentView.mm +86 -0
  17. package/ios/InstareadNativePlayerLegacyView.h +22 -0
  18. package/ios/InstareadNativePlayerLegacyView.m +45 -0
  19. package/ios/InstareadNativePlayerViewManager.h +14 -0
  20. package/ios/InstareadNativePlayerViewManager.m +28 -0
  21. package/ios/InstareadPlayerHost.swift +114 -0
  22. package/ios/InstareadReactNativeModule.h +18 -0
  23. package/ios/InstareadReactNativeModule.mm +48 -0
  24. package/lib/module/InstareadNativePlayerNativeComponent.ts +22 -0
  25. package/lib/module/InstareadPlayer.js +16 -0
  26. package/lib/module/InstareadPlayerView.js +137 -0
  27. package/lib/module/NativeInstareadReactNative.js +7 -0
  28. package/lib/module/index.js +5 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/react-native-deep-imports.d.js +2 -0
  31. package/lib/typescript/package.json +1 -0
  32. package/lib/typescript/src/InstareadNativePlayerNativeComponent.d.ts +14 -0
  33. package/lib/typescript/src/InstareadPlayer.d.ts +11 -0
  34. package/lib/typescript/src/InstareadPlayerView.d.ts +49 -0
  35. package/lib/typescript/src/NativeInstareadReactNative.d.ts +7 -0
  36. package/lib/typescript/src/index.d.ts +4 -0
  37. package/package.json +124 -0
  38. package/src/InstareadNativePlayerNativeComponent.ts +22 -0
  39. package/src/InstareadPlayer.ts +14 -0
  40. package/src/InstareadPlayerView.tsx +219 -0
  41. package/src/NativeInstareadReactNative.ts +9 -0
  42. package/src/index.tsx +3 -0
  43. package/src/react-native-deep-imports.d.ts +21 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,86 @@
1
+ # Changelog
2
+
3
+ ## 1.3.4
4
+
5
+ - Bundles InstareadSDK 1.4.4 (iOS) and 1.3.3 (Android). Both bring the same fix: the mini player
6
+ now follows `mobile_is_floating_player_enabled` being switched on or off once the app is
7
+ reopened, instead of keeping whatever it read when the process first started.
8
+ - Android also reserves a few dp below the height the player page reports, so a bottom border or
9
+ shadow is not clipped on the compact design.
10
+ - No change to this package's own API.
11
+
12
+ ## 1.3.3
13
+
14
+ - Bundles InstareadSDK 1.4.3 (iOS) and 1.3.2 (Android). Both are documentation-only releases of
15
+ the native SDKs — the players themselves are byte-for-byte what 1.4.2 and 1.3.1 shipped, so
16
+ behaviour is unchanged.
17
+ - The README's checksum step is now a command that verifies on its own,
18
+ `shasum -a 256 -c react-native-instaread-sdk-<version>.sha256`, instead of asking you to compare
19
+ two hashes by eye.
20
+ - No change to this package's own API.
21
+
22
+ ## 1.3.2
23
+
24
+ - Bundles InstareadSDK 1.3.1 (Android), which fixes a band of blank space under the player on
25
+ Android — or a clipped player — depending on which design the publication uses. 1.3.1 of this
26
+ package shipped the Android SDK from before that fix.
27
+ - iOS is unaffected and stays on InstareadSDK 1.4.2. No change to this package's own API.
28
+
29
+ ## 1.3.1
30
+
31
+ - Bundles InstareadSDK 1.4.2 (iOS), up from 1.4.0 — which brings 1.4.1's packaging work with it.
32
+ Nothing changed in the native player itself; both of those iOS releases were packaging only.
33
+ - Android stays on InstareadSDK 1.3.0.
34
+ - No change to this package's own API: `<InstareadPlayerView />` takes the same props and behaves
35
+ identically.
36
+
37
+ ## 1.3.0
38
+
39
+ - **Dark mode.** `<InstareadPlayerView dark />` renders the dark player, on both platforms and
40
+ both Android architectures. Your app decides — the SDK never reads the device's appearance,
41
+ because an app with its own in-app theme toggle can be dark while the phone is light. The mini
42
+ player, its playlist sheet and its Now Playing panel follow the article player's palette rather
43
+ than carrying their own.
44
+ - Bundles InstareadSDK 1.4.0 (iOS) and 1.3.0 (Android).
45
+
46
+ ## 1.2.0
47
+
48
+ - **Android.** The package now ships both platforms: the Android half hosts the native
49
+ `InstareadPlayerView` from `co.instaread:sdk` (bundled as an `.aar`) exactly as the iOS half
50
+ hosts `InstareadSDK.xcframework`. Nothing about the player is reimplemented. The JS API is
51
+ unchanged — `InstareadPlayer.configure(...)` and `<InstareadPlayerView articleUrl=... />` work
52
+ the same on both.
53
+ - Supports both Android architectures: a Fabric `ViewManager` with the generated delegate on the
54
+ new architecture, `@ReactProp` on the old one. Verified running on RN 0.79.7 (old) and 0.87.1
55
+ (new).
56
+ - The player's height is reported natively and applied in JS, as on iOS. Reported in dp, so it is
57
+ correct on every screen density.
58
+ - `style` now also ignores `width`/`minWidth`/`maxWidth`, alongside height and flex: the player
59
+ fills the container it is placed in on every platform, and its size is not the app's to set.
60
+ - Bundles InstareadSDK 1.3.3 (iOS) and 1.2.3 (Android) — see `instareadNativeSdk` in
61
+ `package.json`.
62
+
63
+ ## 1.1.0
64
+
65
+ - Bundles InstareadSDK 1.3.3, which adds Apple's privacy manifest (`PrivacyInfo.xcprivacy`) —
66
+ see `instareadNativeSdk` in `package.json`.
67
+ - `InstareadPlayerView` gains `onReady` and `onError`, its only two signals (see the README). No
68
+ `onPlay`/`onPause`/`onEnded`/analytics hook — deliberately not exposed.
69
+ - Supports the old architecture (Paper) as well as the New Architecture, so apps on older React
70
+ Native don't need to upgrade to integrate. Same JS API on both.
71
+ - Widens support down to React Native 0.76 (from 0.84): built and run on 0.76.9, 0.79.7, 0.81.6,
72
+ 0.84.1, 0.86.2 and 0.87.1, Xcode 26, both architectures where the version has one.
73
+ - Codegen spec now imports from React Native's original module paths instead of its newer
74
+ re-exports, for compatibility across that wider version range.
75
+ - `peerDependencies.react-native` lowered to `>=0.76.0`; `react` un-pinned.
76
+ - `InstareadPlayerView`'s `style` prop no longer accepts height/flex sizing — the player's height
77
+ was never actually settable through it, so this only makes the type honest.
78
+ - In development builds, warns to the console instead of failing silently when the native module
79
+ is missing or `UIBackgroundModes` lacks `audio`.
80
+
81
+ ## 1.0.0
82
+
83
+ - First release. iOS only: on Android the view renders nothing and `configure` does nothing.
84
+ - `InstareadPlayer.configure(publication)` and a self-sizing `<InstareadPlayerView articleUrl />`.
85
+ - Bundles InstareadSDK 1.3.2 (see `instareadNativeSdk` in `package.json`).
86
+ - React Native 0.84+ (built and run on 0.84.1, 0.86.2, 0.87.1), Xcode 26.
@@ -0,0 +1,29 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ # The pod is deliberately not called "InstareadSdk": that differs from the native
6
+ # framework "InstareadSDK" only by case, and on a case-insensitive filesystem the
7
+ # two would share build products and module-cache entries.
8
+ Pod::Spec.new do |s|
9
+ s.name = "InstareadReactNative"
10
+ s.version = package["version"]
11
+ s.summary = package["description"]
12
+ s.homepage = "https://instaread.co"
13
+ s.license = { :type => "Proprietary", :file => "LICENSE" }
14
+ s.authors = package["author"]
15
+
16
+ s.platforms = { :ios => min_ios_version_supported }
17
+ # Delivered as a zip and linked from the customer's node_modules; never fetched.
18
+ s.source = { :path => "." }
19
+
20
+ # Not `ios/**`: the XCFramework below carries its own headers.
21
+ s.source_files = "ios/*.{h,m,mm,swift}"
22
+ s.private_header_files = "ios/*.h"
23
+ s.swift_version = "5.0"
24
+
25
+ # The native SDK, unchanged. Put it here with scripts/sync-ios-sdk.sh.
26
+ s.vendored_frameworks = "ios/Frameworks/InstareadSDK.xcframework"
27
+
28
+ install_modules_dependencies(s)
29
+ end
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ Copyright (c) 2026 Instaread. All rights reserved.
2
+
3
+ This software is proprietary to Instaread. It is provided to Instaread
4
+ customers for use in their own applications, under the terms of their agreement
5
+ with Instaread. Without such an agreement no right to use, copy, modify or
6
+ distribute it is granted.
7
+
8
+ The package includes a compiled copy of InstareadSDK, which is covered by the
9
+ same terms.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # react-native-instaread-sdk
2
+
3
+ Audio player for article screens in React Native, on iOS and Android. It hosts the native
4
+ InstareadSDK, which renders Instaread's real web player inside a WebView — so it looks and
5
+ behaves exactly like the web player — publishes it to the lock screen (and the notification
6
+ shade on Android), and shows a floating mini player above your app while something plays.
7
+
8
+ - **Same player as the web.** Nothing is re-implemented in JavaScript, and there is no
9
+ `react-native-webview` dependency.
10
+ - **Read-only.** It plays audio that already exists for an article. It never triggers audio
11
+ generation.
12
+ - **Server-driven.** Player design, colours and whether the mini player is on come from the
13
+ server per publication, so most changes need no app release.
14
+ - **Both platforms, one API.** The same JavaScript runs on iOS and Android, and on both Android
15
+ architectures (New and old/Paper).
16
+
17
+ ## Requirements
18
+
19
+ | | |
20
+ |---|---|
21
+ | React Native | 0.76 or newer, either architecture (New or old/Paper). Built and run on 0.76.9, 0.79.7, 0.81.6, 0.84.1, 0.86.2 and 0.87.1 — old architecture on 0.76–0.81 (the versions that still have one), New Architecture on all six |
22
+ | iOS | 13.0+. Built with Xcode 26 |
23
+ | Android | API 24 (7.0)+ |
24
+ | Your `Info.plist` (iOS) | `UIBackgroundModes` containing `audio` — see below |
25
+ | Android permissions | `FOREGROUND_SERVICE` and `FOREGROUND_SERVICE_MEDIA_PLAYBACK` are merged in for you. `POST_NOTIFICATIONS` on Android 13+ is yours to request — without it audio still plays, but the notification and lock-screen card do not appear |
26
+ | Network | HTTPS to `player.instaread.co` and `player-api.instaread.co`. No ATS exceptions needed |
27
+ | Permissions prompts | None from the SDK. No entitlements |
28
+
29
+ **`UIBackgroundModes` → `audio` is yours to add.** It belongs to your app bundle; the SDK cannot
30
+ grant it. Without it, playback stops and the lock screen goes empty as soon as the screen locks.
31
+
32
+ ```xml
33
+ <key>UIBackgroundModes</key>
34
+ <array>
35
+ <string>audio</string>
36
+ </array>
37
+ ```
38
+
39
+ ## Install
40
+
41
+ You receive `react-native-instaread-sdk-<version>.zip`. Nothing to publish or download from a
42
+ registry; the native SDK is inside.
43
+
44
+ 1. Unzip it into your project, e.g. `vendor/`. Keep the folder inside your project.
45
+ 2. Add it as a dependency:
46
+
47
+ ```sh
48
+ npm install ./vendor/react-native-instaread-sdk
49
+ # yarn: yarn add file:./vendor/react-native-instaread-sdk
50
+ ```
51
+
52
+ 3. Install pods:
53
+
54
+ ```sh
55
+ cd ios && pod install
56
+ ```
57
+
58
+ 4. iOS: add `audio` to `UIBackgroundModes` (above), then rebuild.
59
+ 5. Android: nothing else. Rebuild the app.
60
+
61
+ To verify the download, run `shasum -a 256 -c react-native-instaread-sdk-<version>.sha256` next to the zip. It prints `OK`.
62
+
63
+ ## Use
64
+
65
+ ```tsx
66
+ import { InstareadPlayer, InstareadPlayerView } from 'react-native-instaread-sdk';
67
+
68
+ // Once, at app start — before anything renders.
69
+ InstareadPlayer.configure('yourslug');
70
+
71
+ // Wherever the player goes on an article screen.
72
+ <InstareadPlayerView articleUrl={article.canonicalUrl} />
73
+ ```
74
+
75
+ That is the whole integration. There is no client to create and nothing to pass around: every
76
+ `InstareadPlayerView` and the mini player share the SDK's own state, so pressing Play on one
77
+ article's player shows up in the mini player anywhere else in your app.
78
+
79
+ ## API
80
+
81
+ | | Notes |
82
+ |---|---|
83
+ | `InstareadPlayer.configure(publication)` | Once, at startup. Sets the publication `InstareadPlayerView` uses and shows the mini player in its own window above your app (above your tab bar if you have one). |
84
+ | `<InstareadPlayerView articleUrl publication? style? onReady? onError? />` | `articleUrl` is the article's own page URL, the same one the web player is given. `publication` overrides the configured slug for this one player. `style` is for layout only (margins, alignment) — the height is the player's own. |
85
+
86
+ `InstareadPlayerView` sizes itself: zero height until there is something to show, then the
87
+ loader, then the player. It never scrolls on its own, so place it inside your article's
88
+ `ScrollView`.
89
+
90
+ **`onReady` and `onError`** are the only two signals the player gives your code:
91
+
92
+ - `onReady()` fires once the player has shown something for this article — the player itself, or
93
+ Instaread's "Audio Coming Soon" state. Changing `articleUrl` or `publication` resets it.
94
+ - `onError()` fires once if nothing has rendered about 15 seconds after mounting (or after
95
+ `articleUrl`/`publication` changes) — usually a wrong or not-yet-enabled `publication`, or no
96
+ network. It's a guess made on the React Native side, not a report from the player itself: the
97
+ native SDK never distinguishes a real failure from an unusually slow load, so `onError` can fire
98
+ on a slow connection that would have come through fine a moment later. It never fires once
99
+ `onReady` has.
100
+
101
+ There is deliberately no `onPlay`/`onPause`/`onEnded`/analytics hook: what happens inside the
102
+ player, and the mini player's own behaviour, are not exposed — see *How it behaves* below.
103
+
104
+ ## How it behaves
105
+
106
+ - **`publication`** is your Instaread slug. It must match exactly, and Instaread must have set it
107
+ up for the app SDK.
108
+ - **An article whose audio isn't ready** shows Instaread's "Audio Coming Soon" state. The SDK
109
+ never starts audio processing.
110
+ - **The mini player** only appears for publications where it is switched on (see below). Leaving
111
+ an article screen while it plays keeps the audio going, controlled from the mini player.
112
+
113
+ ## Server-side settings
114
+
115
+ Set per publication by Instaread, not in the app — which is why they change without a release:
116
+
117
+ | Key | Effect |
118
+ |---|---|
119
+ | `mobile_playerType` | Inline design: `acoustic` (default), `compact` or `rectangular` |
120
+ | `mobile_is_floating_player_enabled` | Whether the mini player exists at all. **Off by default — the usual reason a mini player never appears** |
121
+ | `mobile_mini_player_bottom_height` | Extra points the mini bar sits above the bottom. `0` by default. Use it when your bottom UI isn't a native `UITabBar` (React Navigation's bottom tabs, for instance, are not one — ask Instaread to set this) |
122
+
123
+ ## Privacy
124
+
125
+ The native SDK ships Apple's privacy manifest (`PrivacyInfo.xcprivacy`) inside the framework.
126
+ It collects an anonymous device ID (a random UUID kept in the Keychain, not the advertising ID)
127
+ and playback interactions, for analytics and app functionality, not linked to identity and not
128
+ used for tracking. Your own App Store privacy label is still yours to fill in.
129
+
130
+ ## Troubleshooting
131
+
132
+ | Symptom | Likely cause |
133
+ |---|---|
134
+ | Nothing shows | Wrong `publication` slug, or not enabled for the app SDK. Check that `configure` ran before the first `InstareadPlayerView` |
135
+ | Player works, no mini player | `mobile_is_floating_player_enabled` is off for the publication |
136
+ | Audio stops when the screen locks (iOS) | `UIBackgroundModes` → `audio` missing from your `Info.plist` |
137
+ | No notification or lock-screen card (Android 13+) | Your app has not requested `POST_NOTIFICATIONS` |
138
+ | Mini player covers your bottom tab bar | It is not a native `UITabBar`; ask Instaread to set `mobile_mini_player_bottom_height` |
139
+ | `pod install` can't find `InstareadReactNative` | The folder was unzipped outside your project, or `npm install` wasn't re-run after moving it |
140
+
141
+ In a Debug build the native SDK prints reasons prefixed `InstareadSDK:` — to the Xcode console on
142
+ iOS, to logcat on Android. A release build prints nothing.
143
+
144
+ ## License
145
+
146
+ Proprietary. See `LICENSE`.
@@ -0,0 +1,105 @@
1
+ // The Android half of the wrapper. It hosts the native InstareadSDK for
2
+ // Android exactly as the iOS half hosts InstareadSDK.xcframework: the SDK is
3
+ // bundled, used through its public API, and nothing about the player is
4
+ // reimplemented here.
5
+
6
+ buildscript {
7
+ repositories {
8
+ google()
9
+ mavenCentral()
10
+ }
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21"
14
+ }
15
+ }
16
+
17
+ apply plugin: "com.android.library"
18
+ apply plugin: "org.jetbrains.kotlin.android"
19
+ apply plugin: "com.facebook.react"
20
+
21
+ def reactNativeArchitectures() {
22
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
23
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
24
+ }
25
+
26
+ def isNewArchitectureEnabled() {
27
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
28
+ }
29
+
30
+ android {
31
+ namespace "com.instaread.reactnative"
32
+
33
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
34
+
35
+ defaultConfig {
36
+ // The native SDK's own floor; raising it here would raise it for every
37
+ // app that installs this package.
38
+ minSdkVersion safeExtGet("minSdkVersion", 24)
39
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
40
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
41
+ }
42
+
43
+ buildFeatures {
44
+ buildConfig true
45
+ }
46
+
47
+ sourceSets {
48
+ main {
49
+ // The generated ViewManagerDelegate/interface differ between the two
50
+ // architectures, so each gets its own thin ViewManager subclass and the
51
+ // shared code stays in src/main.
52
+ if (isNewArchitectureEnabled()) {
53
+ java.srcDirs += ["src/newarch"]
54
+ } else {
55
+ java.srcDirs += ["src/oldarch"]
56
+ }
57
+ }
58
+ }
59
+
60
+ compileOptions {
61
+ sourceCompatibility JavaVersion.VERSION_17
62
+ targetCompatibility JavaVersion.VERSION_17
63
+ }
64
+
65
+ lintOptions {
66
+ disable "GradleCompatible"
67
+ }
68
+ }
69
+
70
+ kotlin {
71
+ jvmToolchain(17)
72
+ }
73
+
74
+ repositories {
75
+ mavenCentral()
76
+ google()
77
+ }
78
+
79
+ dependencies {
80
+ implementation "com.facebook.react:react-android"
81
+
82
+ // The native SDK, vendored by scripts/sync-android-sdk.sh. A plain file
83
+ // dependency rather than the Maven repository the customer zip ships:
84
+ // a library module that declares its own repositories{} breaks any app whose
85
+ // settings.gradle uses FAIL_ON_PROJECT_REPOS, and this wrapper must not care
86
+ // how a partner's Gradle is set up.
87
+ implementation files("$projectDir/libs/instaread-sdk.aar")
88
+
89
+ // An .aar carries no dependency metadata, so the SDK's own dependencies are
90
+ // declared here instead. sync-android-sdk.sh reads them out of the SDK's POM
91
+ // and fails if this list has drifted — do not edit these by hand.
92
+ implementation "androidx.media:media:1.7.0"
93
+ implementation "androidx.core:core-ktx:1.13.1"
94
+ implementation "androidx.startup:startup-runtime:1.2.0"
95
+ }
96
+
97
+ react {
98
+ jsRootDir = file("../src/")
99
+ libraryName = "InstareadReactNative"
100
+ codegenJavaPackageName = "com.instaread.reactnative"
101
+ }
102
+
103
+ def safeExtGet(prop, fallback) {
104
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
105
+ }
Binary file
@@ -0,0 +1 @@
1
+ 1.3.3
@@ -0,0 +1,6 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- Empty on purpose. Everything the player needs — the FOREGROUND_SERVICE
3
+ permissions, the playback service and the androidx.startup initializer
4
+ — is declared by the native SDK's own manifest and merged in from the
5
+ .aar, exactly as it is for a plain Android app. -->
6
+ </manifest>
@@ -0,0 +1,38 @@
1
+ package com.instaread.reactnative
2
+
3
+ import com.facebook.react.uimanager.SimpleViewManager
4
+ import com.facebook.react.uimanager.ThemedReactContext
5
+
6
+ /**
7
+ * Everything the two architectures share. The only difference between them is
8
+ * how props reach the view — the new architecture goes through a generated
9
+ * delegate, the old one through `@ReactProp` — so each has a thin subclass in
10
+ * src/newarch and src/oldarch and the real work stays here.
11
+ */
12
+ abstract class InstareadNativePlayerViewManagerBase : SimpleViewManager<InstareadPlayerHostView>() {
13
+
14
+ override fun getName() = NAME
15
+
16
+ public override fun createViewInstance(context: ThemedReactContext) =
17
+ InstareadPlayerHostView(context)
18
+
19
+ /** Runs once per update after every prop has been set — see the view's own
20
+ * note on why the player is built here rather than in a setter. */
21
+ override fun onAfterUpdateTransaction(view: InstareadPlayerHostView) {
22
+ super.onAfterUpdateTransaction(view)
23
+ view.commitProps()
24
+ }
25
+
26
+ /** Makes `onHeightChange` reach the JS prop of the same name. */
27
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
28
+ mutableMapOf(
29
+ InstareadPlayerHostView.HEIGHT_CHANGE_EVENT to
30
+ mapOf("registrationName" to InstareadPlayerHostView.HEIGHT_CHANGE_EVENT),
31
+ )
32
+
33
+ companion object {
34
+ /** Must match the name given to codegenNativeComponent in
35
+ * src/InstareadNativePlayerNativeComponent.ts. */
36
+ const val NAME = "InstareadNativePlayer"
37
+ }
38
+ }
@@ -0,0 +1,133 @@
1
+ package com.instaread.reactnative
2
+
3
+ import android.view.View
4
+ import android.widget.FrameLayout
5
+ import co.instaread.sdk.InstareadPlayerView
6
+ import com.facebook.react.bridge.WritableMap
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.facebook.react.uimanager.UIManagerHelper
10
+ import com.facebook.react.uimanager.events.Event
11
+
12
+ /**
13
+ * Hosts one native `InstareadPlayerView` and reports how tall it wants to be —
14
+ * the Android counterpart of InstareadPlayerHost.swift, and the only place this
15
+ * wrapper touches the SDK. It uses the SDK's public API exactly as a plain
16
+ * Android app would and adds nothing to it.
17
+ *
18
+ * The player sizes itself (zero height, then the loading placeholder, then the
19
+ * player), but a React Native view's height comes from JS. So this measures the
20
+ * player after every layout pass and hands the height to `onHeightChange`; JS
21
+ * turns it into the component's height. Same contract as iOS, which reads
22
+ * `intrinsicContentSize` for the same reason.
23
+ */
24
+ class InstareadPlayerHostView(private val reactContext: ThemedReactContext) :
25
+ FrameLayout(reactContext) {
26
+
27
+ private var player: InstareadPlayerView? = null
28
+ private var publication = ""
29
+ private var articleUrl = ""
30
+ private var dark = false
31
+ private var reportedHeightDp = 0.0
32
+ private var pendingPublication = ""
33
+ private var pendingArticleUrl = ""
34
+ private var pendingDark = false
35
+
36
+ // Props arrive one at a time; the player is built once both have landed,
37
+ // from onAfterUpdateTransaction. Building on each setter would load the
38
+ // article twice whenever both change together.
39
+ fun setPublicationProp(value: String?) {
40
+ pendingPublication = value ?: ""
41
+ }
42
+
43
+ fun setArticleUrlProp(value: String?) {
44
+ pendingArticleUrl = value ?: ""
45
+ }
46
+
47
+ fun setDarkProp(value: Boolean) {
48
+ pendingDark = value
49
+ }
50
+
51
+ fun commitProps() {
52
+ if (pendingPublication == publication && pendingArticleUrl == articleUrl && pendingDark == dark) return
53
+ publication = pendingPublication
54
+ articleUrl = pendingArticleUrl
55
+ dark = pendingDark
56
+ rebuildPlayer()
57
+ }
58
+
59
+ private fun rebuildPlayer() {
60
+ player?.let { removeView(it) }
61
+ player = null
62
+ reportedHeightDp = 0.0
63
+
64
+ // Nothing to show without an article; an empty publication falls back
65
+ // to the one passed to InstareadPlayer.configure.
66
+ if (articleUrl.isNotEmpty()) {
67
+ val created = if (publication.isEmpty()) {
68
+ InstareadPlayerView(context, articleUrl, null, dark)
69
+ } else {
70
+ InstareadPlayerView(context, articleUrl, publication, dark)
71
+ }
72
+ addView(created, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
73
+ player = created
74
+ }
75
+ requestLayout()
76
+ }
77
+
78
+ /**
79
+ * React Native lays its own views out and never runs Android's layout pass
80
+ * on their children, so a plain `requestLayout()` from the player — which
81
+ * is how it announces its measured height once the page has rendered —
82
+ * would go nowhere. Re-running measure/layout here is what makes the
83
+ * player's own sizing reach JS at all.
84
+ */
85
+ override fun requestLayout() {
86
+ super.requestLayout()
87
+ post(measureAndLayout)
88
+ }
89
+
90
+ private val measureAndLayout = Runnable {
91
+ measure(
92
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
93
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
94
+ )
95
+ layout(left, top, right, bottom)
96
+ reportHeightIfNeeded()
97
+ }
98
+
99
+ private fun reportHeightIfNeeded() {
100
+ // In density-independent pixels: JS style values are dp, the measured
101
+ // value is px. Sending px would make the player several times too tall
102
+ // on any real handset.
103
+ val density = resources.displayMetrics.density
104
+ val heightDp = ((player?.measuredHeight ?: 0) / density).toDouble()
105
+ if (kotlin.math.abs(heightDp - reportedHeightDp) < 0.5) return
106
+ reportedHeightDp = heightDp
107
+
108
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
109
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
110
+ ?.dispatchEvent(HeightChangeEvent(surfaceId, id, heightDp))
111
+ }
112
+
113
+ /** Dispatched through the EventDispatcher rather than RCTEventEmitter, so
114
+ * the same code path works on both the old and the new architecture. */
115
+ private class HeightChangeEvent(
116
+ surfaceId: Int,
117
+ viewId: Int,
118
+ private val heightDp: Double,
119
+ ) : Event<HeightChangeEvent>(surfaceId, viewId) {
120
+ override fun getEventName() = EVENT_NAME
121
+
122
+ override fun getEventData(): WritableMap =
123
+ Arguments.createMap().apply { putDouble("height", heightDp) }
124
+
125
+ companion object {
126
+ const val EVENT_NAME = "onHeightChange"
127
+ }
128
+ }
129
+
130
+ companion object {
131
+ const val HEIGHT_CHANGE_EVENT = HeightChangeEvent.EVENT_NAME
132
+ }
133
+ }
@@ -0,0 +1,35 @@
1
+ package com.instaread.reactnative
2
+
3
+ import co.instaread.sdk.InstareadPlayer
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
6
+ import com.facebook.react.bridge.ReactMethod
7
+ import com.facebook.react.module.annotations.ReactModule
8
+
9
+ /**
10
+ * `InstareadPlayer.configure(publication)` for JS — the Android half of
11
+ * InstareadBridge in InstareadPlayerHost.swift.
12
+ *
13
+ * Declared as a plain module rather than against the generated TurboModule
14
+ * spec so one class serves both architectures: JS reaches it through
15
+ * `TurboModuleRegistry.get`, which falls back to the legacy registry.
16
+ */
17
+ @ReactModule(name = InstareadReactNativeModule.NAME)
18
+ class InstareadReactNativeModule(reactContext: ReactApplicationContext) :
19
+ ReactContextBaseJavaModule(reactContext) {
20
+
21
+ override fun getName() = NAME
22
+
23
+ @ReactMethod
24
+ fun configure(publication: String) {
25
+ val application = reactApplicationContext.applicationContext as? android.app.Application ?: return
26
+ // The SDK's own entry point, unchanged: sets the publication every
27
+ // player uses and brings up the mini player.
28
+ InstareadPlayer.configure(application, publication)
29
+ }
30
+
31
+ companion object {
32
+ /** Must match the name in src/NativeInstareadReactNative.ts. */
33
+ const val NAME = "InstareadReactNative"
34
+ }
35
+ }
@@ -0,0 +1,20 @@
1
+ package com.instaread.reactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ /**
9
+ * What React Native's autolinking finds. Registers the two things this package
10
+ * exposes: the player view and `InstareadPlayer.configure`.
11
+ */
12
+ class InstareadReactNativePackage : ReactPackage {
13
+
14
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
15
+ listOf(InstareadReactNativeModule(reactContext))
16
+
17
+ override fun createViewManagers(
18
+ reactContext: ReactApplicationContext,
19
+ ): List<ViewManager<*, *>> = listOf(InstareadNativePlayerViewManager())
20
+ }
@@ -0,0 +1,33 @@
1
+ package com.instaread.reactnative
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.ViewManagerDelegate
5
+ import com.facebook.react.viewmanagers.InstareadNativePlayerManagerDelegate
6
+ import com.facebook.react.viewmanagers.InstareadNativePlayerManagerInterface
7
+
8
+ /**
9
+ * New architecture (Fabric). Props arrive through the delegate generated from
10
+ * src/InstareadNativePlayerNativeComponent.ts, so this class only forwards
11
+ * them; everything else is in the shared base.
12
+ */
13
+ @ReactModule(name = InstareadNativePlayerViewManagerBase.NAME)
14
+ class InstareadNativePlayerViewManager :
15
+ InstareadNativePlayerViewManagerBase(),
16
+ InstareadNativePlayerManagerInterface<InstareadPlayerHostView> {
17
+
18
+ private val delegate = InstareadNativePlayerManagerDelegate(this)
19
+
20
+ override fun getDelegate(): ViewManagerDelegate<InstareadPlayerHostView> = delegate
21
+
22
+ override fun setPublication(view: InstareadPlayerHostView, value: String?) {
23
+ view.setPublicationProp(value)
24
+ }
25
+
26
+ override fun setArticleUrl(view: InstareadPlayerHostView, value: String?) {
27
+ view.setArticleUrlProp(value)
28
+ }
29
+
30
+ override fun setDark(view: InstareadPlayerHostView, value: Boolean) {
31
+ view.setDarkProp(value)
32
+ }
33
+ }