npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

reel-acquire-sdk

v1.0.2

Published

Reward users for scrolling videos!

Readme

reel-acquire-sdk

Reward users for scrolling Instagram. The SDK opens a native full-screen WebView, loads an Instagram link, injects a tracking script, and reports activity back to the host app so it can award rewards.

Native-first. Almost all logic lives in the native iOS and Android cores. The React Native / Expo layers are thin wrappers over those cores.

Consumption modes

This package is designed to be used separately or together across four targets:

| Target | Entry point | Mechanism | |--------|-------------|-----------| | Native iOS (Swift) | ios/ReelAcquireCore | Swift Package ReelAcquireCore | | Native Android (Kotlin) | android/core | Gradle lib com.reelacquire:reel-acquire-core | | React Native | src/index.ts → TurboModule ReelAcquire | CocoaPods bridge + :reactnative Gradle module | | Expo | app.plugin.js config plugin | Wires native config during expo prebuild |

The core layers have zero React Native dependency. The RN bridges depend on the cores; they do not recompile them.

Module boundaries

iOS — the core is a Swift Package (ReelAcquireCore). Native apps add it via SPM. For React Native it also ships a co-located podspec (ios/ReelAcquireCore/ReelAcquireCore.podspec) so the thin bridge pod (reel-acquire-sdk) can dependency 'ReelAcquireCore' — a real module boundary, same sources. The Expo plugin adds the core path-pod to the app Podfile.

Android — two Gradle modules:

  • android/corecom.reelacquire:reel-acquire-core (no RN dependency, publishable).
  • android/reactnative → the autolinked bridge (react-native.config.js points here). Depends on :core when built standalone, or the published core artifact when autolinked into an app (./gradlew :core:publishToMavenLocal for local dev).

Architecture

              ┌─────────────────────────────────────────────┐
   JS / TS    │  src/index.ts  (public API + typed events)   │
              │  src/NativeReelAcquire.ts  (TurboModule spec) │
              └───────────────┬───────────────┬──────────────┘
                              │               │
          ┌───────────────────┘               └───────────────────┐
   iOS    │ ios/rn/  (pod: reel-acquire-sdk)   android/reactnative │  Android
  bridge  │   thin wrappers, depend on the cores                   │  bridge
          └───────────────────┬───────────────┬───────────────────┘
                              │  depends on    │  depends on
       ┌──────────────────────┘               └──────────────────────┐
  iOS  │ ios/ReelAcquireCore   (Swift Package)  android/core (Gradle) │ Android
  core │  ReelAcquire · APIClient ·               ReelAcquire ·       │ core
       │  WebViewController · ScriptInjector ·     WebViewActivity ·   │
       │  Models                                   ScriptInjector ·    │
       │                                           Models              │
       └────────────────────────┬──────────────────────────────────┘
                                 │  GET {host}/api/v1/_t
                                 ▼
                  Reel Acquire backend  (returns the tracking JS;
                  fetched at initialize, cached in memory, injected)

Layout

sdk/
├── package.json                 RN library manifest (+ codegen config)
├── react-native.config.js       Points android autolinking at the bridge module
├── reel-acquire-sdk.podspec     iOS RN bridge pod (depends on ReelAcquireCore)
├── app.plugin.js                Expo config plugin entry
├── src/                         TypeScript public API + TurboModule spec
├── ios/
│   ├── ReelAcquireCore/         Swift Package (the core) + ReelAcquireCore.podspec
│   └── rn/                      React Native bridge sources
├── android/
│   ├── core/                    Standalone Gradle library (the core)
│   └── reactnative/             React Native bridge module (depends on :core)
└── plugin/                      Expo config plugin (TypeScript source)

Usage

The flow is: initialize → get the next video → present it. When the video finishes playing, the SDK marks it watched on the server automatically. Only one video is active at a time.

Native Swift:

import ReelAcquireCore

// Defaults to host https://api.reelacquire.com:
ReelAcquire.shared.initialize(ReelAcquireSettings(apiKey: "pk_…", userId: "user-123", logLevel: .debug))

// After session_initialized, fetch and present the next video:
ReelAcquire.shared.nextVideo { result in
    if case .success(let video) = result {
        DispatchQueue.main.async { ReelAcquire.shared.present(video: video) }
    }
}

React Native:

import { initialize, getNextVideo, present, LogLevel } from 'reel-acquire-sdk';

// host defaults to https://api.reelacquire.com
initialize({ apiKey: 'pk_…', userId: 'user-123', logLevel: LogLevel.DEBUG });

const video = await getNextVideo();  // { video: { id, title?, url? }, watchKey }
present(video);                      // opens video.url; auto-marks watched when it ends

// Optional: observe logs yourself (they also go to console.* by default).
const sub = addLogListener((e) => myLogger(e.level, e.message));
// sub.remove();

Logging (end-to-end)

LogLevel is DEBUG | INFO | WARN (a message is emitted when its level ≥ the configured level). The native core filters once, logs via os_log, and fans each entry out to observers. The RN bridge is an observer (RCTEventEmitter) that re-emits ReelAcquireLog events; on the JS side they're routed to console.debug / console.info / console.warn automatically. Native-only hosts can register a ReelAcquireLogObserver instead.

Session

The API key is sent as the X-Api-Key header on every request (not a query param or body field).

On initialize, the native core calls POST {host}/api/v1/sessions with { appuser_id } and stores the returned session in memory for its lifetime (ReelAcquire.shared.session / .sessionKey).

JSON endpoints use a standard envelope: { success: true, data } on success, { success: false, error_messages: [...] } on error (surfaced as APIError.server(messages:)). APIClient.postJSON / putJSON unwrap it, trusting the success flag over the HTTP status. The session payload is parsed from data.session.{key,created_at} and data.appuser.{id,external_id}.

The tracking script reports Instagram login state:

  • logged_in → the SDK links the appuser via PUT /api/v1/appusers/:appuser_id, forwarding the raw event payload ({ session_key, page_url?, payload }); the server reads the Instagram id from it. Sent once per session.
  • not_logged_in → a modal warns that videos won't count as watched until the user logs into Instagram.
  • url_change → if the user is logged in and has navigated away from the active video's URL, a "Come back!" modal prompts them to return (its "Go back" action reloads the active video). URL comparison is by host + path.

When logged_in includes an ig_username, the SDK also spins up a hidden profile WebView (HiddenWebView) pointing at https://instagram.com/<username>, injects the secondary script from GET /api/v1/_s, and routes its messages to a ProfileScriptMessageHandler (debug-logged for now). It's created once per session, off-screen, and torn down on initialize.

Per-WebView message handling is abstracted behind ScriptMessageHandler: the primary (video) WebView uses PrimaryScriptMessageHandler (which routes to ReelAcquire's on* intent methods) and the hidden WebView uses ProfileScriptMessageHandler.

Events

The native core has an event bus (ReelAcquire.shared.events) that fans lifecycle events out to observers; the RN bridge re-emits them on a ReelAcquireEvent channel. The first event is session_initialized (fired after the session POST resolves) — use it to sequence work that needs a session.

Emitted events (each carries a data payload):

| Event | When | data | |-------|------|--------| | session_initialized | Session POST resolved | appuser_id?, external_id? | | session_closed | User closed the experience | — | | video_watched | Server confirmed a watch | video_id, watch_key | | video_liked | Server confirmed a like/unlike | video_id, watch_key, liked |

import { addEventListener, getNextVideo, present, ReelAcquireEventName } from 'reel-acquire-sdk';

const sub = addEventListener(async (event) => {
  if (event.name === ReelAcquireEventName.SessionInitialized) {
    const video = await getNextVideo(); // session is guaranteed ready here
    present(video);                     // auto-marks watched when it ends
  }
});
// sub.remove();

Native hosts can conform to ReelAcquireEventObserver and call ReelAcquire.shared.events.addObserver(_:).

Videos

Once a session exists (both require one — they fail with ReelAcquireError.noSession otherwise):

  • GET /api/v1/videos/watch_nextnextVideo returns a single WatchVideo ({ video: { id, title?, url? }, watchKey }).
  • GET /api/v1/videos/queuevideoQueue returns an ordered [WatchVideo] (each entry the same per-item shape as watch_next; may be empty). Exposed to RN as getVideoQueue(): Promise<WatchVideo[]>.
  • present(video:) opens video.url full-screen and makes it the single active video. When the injected script reports video_watched, the SDK automatically POST /api/v1/videos/:videoId/watched with the active video's watchKey. On the first presentation, a one-time onboarding modal (IntroOverlayView) frosts the screen and explains the loop in three steps; "Watch" dismisses it. It's persisted per user in UserDefaults (com.reelacquire.introShown.<userId>), so it only ever shows once.
  • Per-video actions all POST /api/v1/videos/:videoId/<action> with videoId in the path: watched, liked, commented, shared, and broken (markWatched / markLiked / markCommented / markShared / markBroken; the overlay's "Video isn't loading" reports broken).

So the normal flow is just get next → present; marking watched happens on its own when playback ends. There is one active video at a time — presenting another replaces it.

Reward overlay. Watch progress is shown by a floating "Pulse pill" (RewardOverlayView) above the WebView — a frosted capsule with a hue-shifting progress ring and a watched / total count (the total comes from a one-time videos/queue fetch). When the server confirms a watch the pill springs, flashes a "+1 ✦" spark, and advances the ring. The pill has three interactive modes:

  • Watching (a video is showing) → a "(?)" button opens a context menu whose "Video isn't loading" action skips the current video and offers the next.
  • Next available → the pill shows "Next ▸" and the whole pill is tappable to load that video into the same WebView (loop repeats).
  • Completed (no video left) → the pill blooms (spark burst + glow) into an "All caught up ✦" state.

(The "come back" / "log in to Instagram" notices remain UIAlertController modals.)

Tracking script

The injected script is no longer bundled with the SDK. At initialize, the native core fetches it from GET {host}/api/v1/_t (via APIClient, with .reloadIgnoringLocalCacheData so each run gets the latest script), caches it in memory for the process lifetime, and injects it into the WebView. When the script is cached it runs as a WKUserScript at document start (before the page's own scripts, so it can intercept navigation/login early); on a cold first run it's injected via evaluateJavaScript once the network fetch returns. The script itself lives in the backend project and must defer DOM-dependent work until the document is ready.

Development

npm install
npm run typecheck       # type-check the JS layer
npm run build:plugin    # compile the Expo config plugin