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

expo-tnk

v1.4.1

Published

Tnk Factory Offerwall SDK module for Expo

Readme

expo-tnk

Tnk Factory Reward SDK (Android) offerwall module for Expo.

  • iOS: TnkRwdSdk2 v5.88 (vendored xcframework)
  • Android: com.tnkfactory:rwd 8.09.07

Two ways to serve ads:

  1. Offerwall — the SDK's full-screen campaign list, with items re-skinned natively (icon / brand caption / bold action title / primary reward button).
  2. Self-rendered campaigns — placement campaign lists exposed as plain data (or the ready-made <CampaignList />), rendered directly in React Native.

The public API is aligned with expo-adpopcorn: setUserId / openOfferwall(options?) / addOfferwallClosedListener / loadPlacements / participateCampaign(campaign) / openCampaignDetail(campaign) / getTotalReward / <CampaignList /> share the same names and shapes.

Installation

npx expo install expo-tnk

Configuration

Add the config plugin to your app config:

{
  "expo": {
    "plugins": [
      [
        "expo-tnk",
        {
          "iosAppId": "YOUR_IOS_APP_ID",
          "androidAppId": "YOUR_ANDROID_APP_ID"
        }
      ]
    ]
  }
}

| Prop | Platform | Description | | --- | --- | --- | | iosAppId | iOS | Tnk app id issued on the Tnk console. Written to Info.plist as tnkad_app_id; the SDK initializes itself from it, so there is no runtime init call. | | androidAppId | Android | Same, written as <meta-data> to AndroidManifest. | | photoLibraryDescription | iOS | Optional. Adds NSPhotoLibraryUsageDescription. Required if the publisher serves ads that ask the user to attach a proof image — the SDK opens the photo picker for those and iOS crashes without the key. | | cameraDescription | iOS | Optional. Adds NSCameraUsageDescription. Same reason as above. |

Everything else ships with the library itself: Android permissions (INTERNET, ACCESS_WIFI_STATE, AD_ID) and the AdWallActivity declaration. The plugin also adds the Tnk maven repository to the project-level build.gradle — Gradle resolves com.tnkfactory:rwd against the app's repository list, so the repository declared inside the library module alone is not enough. If you manage android/ yourself without prebuild, add it manually:

allprojects {
  repositories {
    maven { url 'https://repository.tnkad.net:8443/repository/public/' }
  }
}

ATT is intentionally not handled here — request tracking permission in the host app.

Then rebuild the native projects:

npx expo prebuild

Testing: while the app is in test state on the Tnk console, ads only appear on registered test devices.

Quick start

import * as Tnk from "expo-tnk";

// Required once, before any other call — ads are not served without a user id.
await Tnk.setUserId(hashedUserId);

// Full-screen offerwall
await Tnk.openOfferwall();

// Inline campaign list
<Tnk.CampaignList placementIds={["main_feed"]} />;

Usage

1. Set the user id (required)

await Tnk.setUserId("user-1234");
  • Must be called before the offerwall or any placement is used; without it the SDK serves no ads at all.
  • The value is delivered back to your server in reward postbacks (md_user_nm), so use the same stable id your backend knows.
  • Hash it first if it is personal information (email, phone number).

2. Offerwall

await Tnk.openOfferwall({
  title: "충전소", // iOS navigation title
});
  • Resolves when the offerwall is presented, not when it closes.
  • title is iOS only; the Android offerwall activity manages its own header.
  • Campaign list items are rendered natively with the library's built-in design; the SDK's top menu/filter bars are kept as-is. Curation sections are disabled on Android to keep the list flat.

Events:

useEffect(() => {
  const closed = Tnk.addOfferwallClosedListener(() => {
    refreshPointBalance(); // typical use: re-query points when the user returns
  });
  const clicked = Tnk.addCampaignClickedListener(({ appId, appName }) => {
    analytics.track("offerwall_campaign_click", { appId, appName });
  });
  return () => {
    closed.remove();
    clicked.remove();
  };
}, []);
  • addOfferwallClosedListener — fired when the offerwall screen closes. On Android the SDK has no close callback, so this is detected when the host activity returns to the foreground after the offerwall was opened.
  • addCampaignClickedListener — purely informational (analytics). The SDK fully handles the click itself (detail page, store landing, reward flow); you never need to act on this event.

Offerwall entry button:

const { count, total } = await Tnk.getTotalReward();
// e.g. hide the entry button when count === 0,
// or show "지금 3,200원 적립 가능!" from total

3. Self-rendered campaigns

Placements are configured per-surface on the Tnk console ([관리자] → [매체관리] → [지면설정]). The library loads them headlessly and hands you plain data — no native view is shown, so everything is rendered 100% in React Native.

The flow is: loadPlacements (list data, which also carries everything a detail screen needs) → participateCampaign (the CTA action) — with openCampaignDetail as the SDK-UI fallback. <CampaignList /> bundles the flow into one component.

<CampaignList />

<Tnk.CampaignList
  placementIds={["main_feed", "main_feed_2"]}
  scrollEnabled={false} // when embedded in an outer ScrollView
  onLoad={(campaigns) => console.log(campaigns.length)}
  onError={(error) => console.warn(error)}
/>

| Prop | Default | Description | | --- | --- | --- | | placementIds | (required) | Placement ids from the Tnk console. The server caps each placement at 20 campaigns, so several placements are merged into one list (deduped by appId, in the given order). Reloads when it changes. | | renderItem | built-in item | (campaign: TnkCampaign) => ReactElement \| null. Replaces the default item UI. | | onItemPress | participateCampaign | (campaign: TnkCampaign) => void. Replaces the default press behavior — e.g. navigate to your own detail screen. | | onLoad | — | Called with the merged TnkCampaign[]. | | onError | — | Called when loading or the default press action fails. | | scrollEnabled | FlatList default | Pass false inside another scroll view. | | style / contentContainerStyle | — | Forwarded to the underlying FlatList. |

The default item matches the offerwall's native item design (icon / brand caption / bold action phrase / reward button), and the default press participates directly (the SDK consent alert may appear once — see below). Custom rendering keeps the default press behavior only if you wire it yourself:

<Tnk.CampaignList
  placementIds={["main_feed"]}
  onItemPress={(campaign) => router.push(`/campaign/${campaign.appId}`)}
  renderItem={(campaign) => (
    <MyCampaignRow
      icon={campaign.iconUrl || campaign.imageUrl}
      brand={campaign.title}
      action={Tnk.campaignActionText(campaign.campaignType)}
      reward={`${campaign.reward.toLocaleString()}${campaign.rewardUnit}`}
    />
  )}
/>

Note: with a custom renderItem you own the press handling — wrap your row in a Pressable and either pass onItemPress or call Tnk.participateCampaign(campaign) yourself.

Headless

const campaigns = await Tnk.loadPlacements(["main_feed", "main_feed_2"]);
// campaigns: TnkCampaign[] — the placements merged into one deduped list.
// Placements that fail to load are skipped; rejects only when every one fails.

Render the list — and a detail screen, if you want one — from the campaign data (title, description, reward, rewardUnit, campaignType, …), then run the CTA action when the user taps join:

<Button
  title={`${campaign.reward}${campaign.rewardUnit} 받기`}
  onPress={async () => {
    try {
      await Tnk.participateCampaign(campaign);
    } catch (error) {
      // consent declined / ended / already completed
    }
  }}
/>
  • participateCampaign registers the participation and opens the store page or the advertiser landing — no SDK UI is shown.
  • Privacy consent is required before participating. By default the SDK's consent alert appears once; to keep the flow fully custom, collect consent in your own UI and call Tnk.setPrivacyConsent(true) beforehand — then no SDK UI is ever shown.
  • The 참여방식/유의사항 texts on the SDK's detail page are client-side templates, not per-ad server data — build your own from campaignType (the screenshots of other offerwall apps do exactly this). campaign.description may be empty — fall back to a campaignActionText(campaign.campaignType)-based template.

SDK detail page fallback

Multi-reward (isMultiReward) campaigns have per-step sub-rewards that only the SDK detail page can enumerate; participateCampaign joins the currently active step. Route those campaigns to the SDK page instead:

try {
  await Tnk.openCampaignDetail(campaign);
} catch (error) {
  // ended / already completed / privacy consent declined (iOS)
}

TnkCampaign fields:

type TnkCampaign = {
  appId: number; // unique campaign id
  placementId: string; // placement the campaign was loaded from
  title: string; // brand title, "[...]"/"(...)" annotations stripped
  imageUrl: string; // creative image — can be a wide banner
  iconUrl: string; // square icon; may be empty — fall back to imageUrl
  reward: number; // reward amount (multiplier events applied)
  originalReward: number; // pre-multiplier reward, 0 outside events
  rewardUnit: string; // e.g. "원"
  campaignType: number; // e.g. 206 — see campaignActionText()
  campaignTypeName: string; // server-provided type name, e.g. "유튜브 구독"
  description: string; // participation description; may be empty — provide a template fallback
  isMultiReward: boolean;
  productPrice: number; // CPS only
  originalProductPrice: number; // CPS only
  discountRate: number; // CPS only, percent
  isFavorite: boolean; // CPS only
};

Built-in campaign item design

Offerwall items (native) and the CampaignList default item share one design, fixed to light theme:

  • Brand caption — 13pt #4B5563, the campaign title with [...]/(...) annotations stripped (e.g. "캣스토랑 : 고양이 셰프 (멀티액션)""캣스토랑 : 고양이 셰프").
  • Action title — 16pt semibold #1F2937, derived from the campaign type code (206"구독하기", 100"설치하기", fallback "확인하기"). The same mapping is exported as campaignActionText().
  • Reward button#3B82F6, radius 6, 34pt tall, white 13pt semibold, {reward}{unit} (e.g. 180원). In the offerwall, non-actionable states dim the button and show a status label instead (설치 확인 / 적립 완료 / 종료 / 내일 가능).

Rewards

Reward payout happens server-to-server: configure the callback URL on the Tnk console ([매체 관리] → 포인트 관리 → 자체서버에서 관리). Tnk calls your server with seq_id / pay_pnt / md_user_nm / md_chk on every payout — deduplicate by seq_id and verify md_chk (md5(app_key + md_user_nm + seq_id)). No reward amount is ever delivered to the client.

API reference

setUserId(userId: string): Promise<void>;

openOfferwall(options?: OfferwallOptions): Promise<void>; // { title? } — iOS only
addOfferwallClosedListener(listener: () => void): EventSubscription;
addCampaignClickedListener(listener: (event: CampaignClickedEvent) => void): EventSubscription;
getTotalReward(): Promise<{ count: number; total: number }>;

loadPlacements(placementIds: string[]): Promise<TnkCampaign[]>;
participateCampaign(campaign: TnkCampaign): Promise<void>; // headless CTA action
openCampaignDetail(campaign: TnkCampaign): Promise<void>; // SDK detail page fallback
<CampaignList placementIds renderItem? onItemPress? onLoad? onError? scrollEnabled? style? />;

setPrivacyConsent(agreed: boolean): void;
campaignActionText(campaignType: number): string;

All promise-returning APIs reject with a coded error (CodedException on Android, Exception on iOS) whose message includes the SDK error detail. Everything is a no-op or throws UnavailabilityError on web.

Example

See example/App.tsx. Fill in the app ids in example/app.json and PLACEMENT_ID in App.tsx, then:

cd example
pnpm install
pnpm ios # or pnpm android

License

MIT