@kissmyappss/react-native 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +178 -2
- package/lib/client.d.ts +127 -0
- package/lib/client.js +511 -0
- package/lib/device.d.ts +2 -0
- package/lib/device.js +59 -0
- package/lib/index.d.ts +33 -0
- package/lib/index.js +89 -0
- package/lib/navigation.d.ts +10 -0
- package/lib/navigation.js +18 -0
- package/package.json +34 -4
- package/src/client.ts +598 -0
- package/src/device.ts +59 -0
- package/src/index.ts +88 -0
- package/src/navigation.ts +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KissMyApps
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,179 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://kissmyapps.dev">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/onorhime/kissmyapps-mobile/main/.github/assets/kissmyapps.png" width="88" height="88" alt="KissMyApps">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
<h1 align="center">KissMyApps for React Native</h1>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
Installs, sessions, screens and your own events, from your app to your KissMyApps dashboard.<br>
|
|
11
|
+
Plain JavaScript: nothing to link, works with Expo.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/@kissmyappss/react-native"><img src="https://img.shields.io/npm/v/@kissmyappss/react-native?color=ff80bf&labelColor=222222" alt="npm"></a>
|
|
16
|
+
<img src="https://img.shields.io/badge/React%20Native-0.70%2B-ff80bf?logo=react&logoColor=white&labelColor=222222" alt="React Native 0.70+">
|
|
17
|
+
<img src="https://img.shields.io/badge/Expo-ready-ff80bf?logo=expo&logoColor=white&labelColor=222222" alt="Expo">
|
|
18
|
+
<img src="https://img.shields.io/badge/TypeScript-included-ff80bf?logo=typescript&logoColor=white&labelColor=222222" alt="TypeScript">
|
|
19
|
+
<img src="https://img.shields.io/badge/license-MIT-ff80bf?labelColor=222222" alt="MIT licence">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Why this SDK
|
|
25
|
+
|
|
26
|
+
- **Start it and you are done.** Installs, updates, opens, time in the app and sessions are
|
|
27
|
+
recorded on their own; React Navigation screens take one line.
|
|
28
|
+
- **Nothing is lost offline.** Events wait in storage, survive a restart and go out with
|
|
29
|
+
backoff once there is a connection.
|
|
30
|
+
- **No native code.** Pure JavaScript with TypeScript types, so it works in Expo Go and needs
|
|
31
|
+
no `pod install`.
|
|
32
|
+
- **Respectful by default.** A random ID per install, no advertising ID, no fingerprinting,
|
|
33
|
+
and an opt-out that is remembered across launches.
|
|
34
|
+
- **One dashboard for every platform.** It sends the same events as the
|
|
35
|
+
[iOS and Android SDKs](https://github.com/onorhime/kissmyapps-mobile) and the web script.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install @kissmyappss/react-native @react-native-async-storage/async-storage
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With Expo, `npx expo install @kissmyappss/react-native @react-native-async-storage/async-storage`.
|
|
44
|
+
|
|
45
|
+
You need React Native 0.70 or later, and a **client key** from your project in the
|
|
46
|
+
KissMyApps console under **Settings → API keys**. Keys starting `kma_pk_live_` count as
|
|
47
|
+
production; keys starting `kma_pk_test_` are kept apart as development.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
Start the SDK once, as early as possible, for example at the top of `App.tsx`:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
55
|
+
import { KissMyApps } from '@kissmyappss/react-native'
|
|
56
|
+
|
|
57
|
+
KissMyApps.init({
|
|
58
|
+
key: 'kma_pk_live_…',
|
|
59
|
+
storage: AsyncStorage,
|
|
60
|
+
appVersion: '2.4.0', // for example DeviceInfo.getVersion()
|
|
61
|
+
build: '120', // and DeviceInfo.getBuildNumber()
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary>Version and build with Expo</summary>
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import * as Application from 'expo-application'
|
|
70
|
+
|
|
71
|
+
KissMyApps.init({
|
|
72
|
+
key: 'kma_pk_live_…',
|
|
73
|
+
storage: AsyncStorage,
|
|
74
|
+
appVersion: Application.nativeApplicationVersion ?? undefined,
|
|
75
|
+
build: Application.nativeBuildVersion ?? undefined,
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
Storage is what lets the SDK recognise a returning person and keep events through a restart.
|
|
82
|
+
Without it the SDK still works, but every launch counts as a new install.
|
|
83
|
+
|
|
84
|
+
## Track what matters
|
|
85
|
+
|
|
86
|
+
### Screens
|
|
87
|
+
|
|
88
|
+
With React Navigation, pass the helper to the container and every screen is recorded under
|
|
89
|
+
its route name:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'
|
|
93
|
+
import { trackNavigation } from '@kissmyappss/react-native'
|
|
94
|
+
|
|
95
|
+
export function App() {
|
|
96
|
+
const navigation = useNavigationContainerRef()
|
|
97
|
+
return (
|
|
98
|
+
<NavigationContainer ref={navigation} {...trackNavigation(navigation)}>
|
|
99
|
+
{/* your navigators */}
|
|
100
|
+
</NavigationContainer>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Without React Navigation, call `KissMyApps.screen('Lesson')` when a screen appears.
|
|
106
|
+
|
|
107
|
+
### Events and people
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
KissMyApps.track('lesson_completed', { lesson: 3, score: 0.92 })
|
|
111
|
+
|
|
112
|
+
KissMyApps.identify('user-42', { email: 'amaka@example.com', plan: 'annual' })
|
|
113
|
+
KissMyApps.setTraits({ plan: 'monthly' })
|
|
114
|
+
KissMyApps.reset() // on sign out: what follows belongs to a new, anonymous person
|
|
115
|
+
|
|
116
|
+
KissMyApps.optOut() // stops sending, remembered across launches
|
|
117
|
+
KissMyApps.optIn()
|
|
118
|
+
await KissMyApps.flush()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Event names use letters, numbers and `_ . : -`, start with a letter and are up to 80
|
|
122
|
+
characters; other names are ignored. Calls made before `init` are kept and run once it is
|
|
123
|
+
called.
|
|
124
|
+
|
|
125
|
+
### Everything at a glance
|
|
126
|
+
|
|
127
|
+
| Call | What it does |
|
|
128
|
+
| ---------------------------------- | ----------------------------------------------------- |
|
|
129
|
+
| `KissMyApps.init(options)` | Starts recording. Call once. |
|
|
130
|
+
| `KissMyApps.track(name, props?)` | Records one of your events. |
|
|
131
|
+
| `KissMyApps.screen(name, props?)` | Records a screen view. |
|
|
132
|
+
| `trackNavigation(ref)` | Records every React Navigation screen. |
|
|
133
|
+
| `KissMyApps.identify(id, traits?)` | Ties this install to your user ID. |
|
|
134
|
+
| `KissMyApps.setTraits(traits)` | Updates what you know about the person. |
|
|
135
|
+
| `KissMyApps.reset()` | Forgets the person and starts a new anonymous one. |
|
|
136
|
+
| `KissMyApps.optOut()` / `optIn()` | Stops or resumes sending. Remembered across launches. |
|
|
137
|
+
| `KissMyApps.flush()` | Sends what is queued now; returns a promise. |
|
|
138
|
+
| `KissMyApps.getAnonymousId()` | This install's random ID, for support tickets. |
|
|
139
|
+
| `KissMyApps.getSessionId()` | The current session's ID. |
|
|
140
|
+
|
|
141
|
+
## What gets recorded
|
|
142
|
+
|
|
143
|
+
| Event | When | Properties |
|
|
144
|
+
| ------------------ | ------------------------------------------ | -------------------------------------------------------- |
|
|
145
|
+
| `session_started` | A new session begins | – |
|
|
146
|
+
| `app_installed` | The first launch after install | `version`, `build` |
|
|
147
|
+
| `app_updated` | The first launch of a new version or build | `version`, `build`, `previous_version`, `previous_build` |
|
|
148
|
+
| `app_opened` | The app comes to the foreground | `from_background` |
|
|
149
|
+
| `app_backgrounded` | The app leaves the foreground | `seconds_in_foreground` |
|
|
150
|
+
| `screen_viewed` | A screen appears | `name`, `previous`, plus your own |
|
|
151
|
+
|
|
152
|
+
Every request also carries iOS or Android and the OS version, phone or tablet, the device
|
|
153
|
+
model on Android (iPhone or iPad on iOS), screen size, language, time zone, app version and
|
|
154
|
+
the SDK version. Country, region and city are worked out by the KissMyApps server from the
|
|
155
|
+
request's address, which is only ever kept as a salted hash. There is no advertising ID.
|
|
156
|
+
|
|
157
|
+
A session ends after 30 minutes in the background or without events. Events are sent every
|
|
158
|
+
10 seconds, at once when 30 are waiting, and when the app goes to the background. While
|
|
159
|
+
offline they wait in storage (up to 1,000, oldest dropped first) and are retried with
|
|
160
|
+
backoff, including on the next launch.
|
|
161
|
+
|
|
162
|
+
## Options
|
|
163
|
+
|
|
164
|
+
| Option | Default | What it does |
|
|
165
|
+
| ----------------------- | ------------------------------- | -------------------------------------------------------------- |
|
|
166
|
+
| `key` | Required | The project's client key. |
|
|
167
|
+
| `storage` | None | AsyncStorage or anything with `getItem` and `setItem`. |
|
|
168
|
+
| `appVersion` | None | The version shown in the dashboard. |
|
|
169
|
+
| `build` | None | The build number, used to spot updates. |
|
|
170
|
+
| `endpoint` | `https://events.kissmyapps.dev` | Where events are sent. |
|
|
171
|
+
| `sessionTimeoutMinutes` | `30` | Minutes in the background or idle before a new session starts. |
|
|
172
|
+
| `flushIntervalSeconds` | `10` | How often queued events are sent while the app is open. |
|
|
173
|
+
| `maxQueue` | `1000` | The most events kept while offline. |
|
|
174
|
+
| `trackLifecycle` | `true` | Record installs, updates, opens and backgrounding. |
|
|
175
|
+
| `debug` | `false` | Log refused events and other problems to the console. |
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT © KissMyApps
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export declare const SDK_VERSION = "1.0.0";
|
|
2
|
+
export declare const DEFAULT_ENDPOINT = "https://events.kissmyapps.dev";
|
|
3
|
+
export type Properties = Record<string, unknown>;
|
|
4
|
+
/** AsyncStorage, MMKV or anything with the same two methods. */
|
|
5
|
+
export interface Storage {
|
|
6
|
+
getItem(key: string): Promise<string | null> | string | null;
|
|
7
|
+
setItem(key: string, value: string): Promise<void> | void;
|
|
8
|
+
}
|
|
9
|
+
export interface DeviceInfo {
|
|
10
|
+
platform: 'ios' | 'android';
|
|
11
|
+
os_name: string;
|
|
12
|
+
os_version: string;
|
|
13
|
+
device_type: 'mobile' | 'tablet';
|
|
14
|
+
device_model?: string;
|
|
15
|
+
screen_width: number;
|
|
16
|
+
screen_height: number;
|
|
17
|
+
timezone: string;
|
|
18
|
+
language: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ClientOptions {
|
|
21
|
+
/** The project's client key, kma_pk_live_… or kma_pk_test_…. */
|
|
22
|
+
key: string;
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
/** The app's version, such as 2.4.0, and build number, such as 120. */
|
|
25
|
+
appVersion?: string;
|
|
26
|
+
build?: string;
|
|
27
|
+
/** A session ends after this long in the background or without events. Default 30. */
|
|
28
|
+
sessionTimeoutMinutes?: number;
|
|
29
|
+
/** How often queued events are sent while the app is open. Default 10. */
|
|
30
|
+
flushIntervalSeconds?: number;
|
|
31
|
+
/** The most events kept while offline; the oldest go first. Default 1000. */
|
|
32
|
+
maxQueue?: number;
|
|
33
|
+
/** Record installs, updates, opens and backgrounding. Default true. */
|
|
34
|
+
trackLifecycle?: boolean;
|
|
35
|
+
debug?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface Environment {
|
|
38
|
+
storage: Storage | null;
|
|
39
|
+
device(): DeviceInfo;
|
|
40
|
+
fetch: (url: string, init: RequestInit) => Promise<Pick<Response, 'ok' | 'status' | 'text'>>;
|
|
41
|
+
sdk: string;
|
|
42
|
+
now?: () => number;
|
|
43
|
+
log?: (message: string) => void;
|
|
44
|
+
}
|
|
45
|
+
export interface QueuedEvent {
|
|
46
|
+
name: string;
|
|
47
|
+
insert_id: string;
|
|
48
|
+
time: string;
|
|
49
|
+
screen?: string;
|
|
50
|
+
properties: Properties;
|
|
51
|
+
user_id: string | null;
|
|
52
|
+
anonymous_id: string;
|
|
53
|
+
session_id: string;
|
|
54
|
+
session_started_at: string;
|
|
55
|
+
}
|
|
56
|
+
export declare const EVENT_NAME: RegExp;
|
|
57
|
+
export declare function uuid(): string;
|
|
58
|
+
export declare class KmaClient {
|
|
59
|
+
readonly options: ClientOptions & {
|
|
60
|
+
endpoint: string;
|
|
61
|
+
};
|
|
62
|
+
private readonly env;
|
|
63
|
+
private readonly storageKey;
|
|
64
|
+
private state;
|
|
65
|
+
private queue;
|
|
66
|
+
private screenName;
|
|
67
|
+
private foregroundAt;
|
|
68
|
+
private loaded;
|
|
69
|
+
private readonly waiting;
|
|
70
|
+
private flushTimer;
|
|
71
|
+
private heartbeatTimer;
|
|
72
|
+
private saveTimer;
|
|
73
|
+
private inflight;
|
|
74
|
+
private failures;
|
|
75
|
+
private lastSent;
|
|
76
|
+
private identityPending;
|
|
77
|
+
private inForeground;
|
|
78
|
+
private firstLaunch;
|
|
79
|
+
private stopped;
|
|
80
|
+
/** Settles once saved state is read and the launch is recorded. */
|
|
81
|
+
readonly ready: Promise<void>;
|
|
82
|
+
constructor(options: ClientOptions, env: Environment);
|
|
83
|
+
/** Records something a person did. */
|
|
84
|
+
track(name: string, properties?: Properties): void;
|
|
85
|
+
/** Records a screen view; later events carry the screen's name. */
|
|
86
|
+
screen(name: string, properties?: Properties): void;
|
|
87
|
+
/** Ties this install to your own user ID, with optional traits such as email, name or plan. */
|
|
88
|
+
identify(userId: string | number, traits?: Properties): void;
|
|
89
|
+
/** Updates traits for the current person without changing who they are. */
|
|
90
|
+
setTraits(traits: Properties): void;
|
|
91
|
+
/** Forgets the user, for example on sign out: what follows is a new person and session. */
|
|
92
|
+
reset(): void;
|
|
93
|
+
/** Stops recording and drops anything not sent yet, until optIn. Remembered across launches. */
|
|
94
|
+
optOut(): void;
|
|
95
|
+
optIn(): void;
|
|
96
|
+
getAnonymousId(): string;
|
|
97
|
+
getUserId(): string | null;
|
|
98
|
+
getSessionId(): string | null;
|
|
99
|
+
/** Sends everything queued now. */
|
|
100
|
+
flush(): Promise<void>;
|
|
101
|
+
/** Stops timers; the client records nothing more. */
|
|
102
|
+
shutdown(): void;
|
|
103
|
+
/** The app came to the front. The first call is the launch. */
|
|
104
|
+
foreground(): void;
|
|
105
|
+
/** The app went to the background: record it and send what is queued while it can. */
|
|
106
|
+
background(): void;
|
|
107
|
+
private load;
|
|
108
|
+
private whenLoaded;
|
|
109
|
+
private recordInstallOrUpdate;
|
|
110
|
+
private save;
|
|
111
|
+
private writeNow;
|
|
112
|
+
private now;
|
|
113
|
+
private ensureSession;
|
|
114
|
+
private enqueue;
|
|
115
|
+
private clean;
|
|
116
|
+
private context;
|
|
117
|
+
/** One request's body: queued events share a person and session; with none, a heartbeat. */
|
|
118
|
+
body(events: QueuedEvent[]): Properties;
|
|
119
|
+
private nextBatch;
|
|
120
|
+
private post;
|
|
121
|
+
private sendQueued;
|
|
122
|
+
private retryLater;
|
|
123
|
+
private scheduleFlush;
|
|
124
|
+
/** Keeps an open app counted as online. */
|
|
125
|
+
private heartbeat;
|
|
126
|
+
private warn;
|
|
127
|
+
}
|