-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathuseSuperwall.ts
More file actions
457 lines (409 loc) · 16.3 KB
/
useSuperwall.ts
File metadata and controls
457 lines (409 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
import { createContext, useContext } from "react"
import { create } from "zustand"
import { useShallow } from "zustand/shallow"
import pkg from "../package.json"
import SuperwallExpoModule from "./SuperwallExpoModule"
import type {
EntitlementsInfo,
IntegrationAttributes,
SubscriptionStatus,
} from "./SuperwallExpoModule.types"
import { DefaultSuperwallOptions, type PartialSuperwallOptions } from "./SuperwallOptions"
/**
* @category Models
* @since 0.0.15
* Interface representing the attributes of a user.
*/
export interface UserAttributes {
/** The user's alias ID, if set. */
aliasId: string
/** The user's application-specific user ID. */
appUserId: string
/** The ISO 8601 date string representation of when the application was installed on the user's device. */
applicationInstalledAt: string
/** A seed value associated with the user, used for consistent variant assignments in experiments. */
seed: number
/** Allows for custom attributes to be set for the user. These can be of any type. */
[key: string]: any
}
/**
* @category Models
* @since 0.0.15
* Options for the `identify` method.
*/
export interface IdentifyOptions {
/**
* Determines whether to restore paywall assignments from a previous session for the identified user.
* If `true`, the SDK attempts to restore the assignments. Defaults to `false`.
*/
restorePaywallAssignments?: boolean
}
/**
* @category Store
* @since 0.0.15
* Defines the structure of the Superwall store, including its state and actions.
* This store is managed by Zustand.
*/
export interface SuperwallStore {
/* -------------------- State -------------------- */
/** Indicates whether the Superwall SDK has been successfully configured. */
isConfigured: boolean
/** Indicates whether the SDK is currently performing a loading operation (e.g., configuring, fetching data). */
isLoading: boolean
/** Indicates whether the native event listeners have been initialized. */
listenersInitialized: boolean
/**
* Contains error message if SDK configuration failed, `null` otherwise.
* When this is set, the SDK is not configured and app should show error UI.
*/
configurationError: string | null
/**
* The current user's attributes.
* `null` if no user is identified or after `reset` is called.
* `undefined` initially before any user data is fetched or set.
*/
user?: UserAttributes | null
/** The current subscription status of the user. */
subscriptionStatus: SubscriptionStatus
/* -------------------- Internal -------------------- */
// Internal listener references for cleanup handled inside Provider effect.
// Not reactive, so we store outside Zustand state to avoid unnecessary rerenders.
/* -------------------- Actions -------------------- */
/**
* Configures the Superwall SDK with the provided API key and options.
* This must be called before most other SDK functions can be used.
* @param apiKey - Your Superwall API key.
* @param options - Optional configuration settings for the SDK.
* @returns A promise that resolves when configuration is complete.
* @internal
*/
configure: (
apiKey: string,
options?: PartialSuperwallOptions & {
/** @deprecated Use manualPurchaseManagement instead */
manualPurchaseManagment?: boolean
},
) => Promise<void>
/**
* Identifies the current user with a unique ID.
* @param userId - The unique identifier for the user.
* @param options - Optional parameters for identification.
* @returns A promise that resolves when identification is complete.
*/
identify: (userId: string, options?: IdentifyOptions) => Promise<void>
/**
* Resets the user's identity and clears all user-specific data, effectively logging them out.
* @internal
*/
reset: () => Promise<void>
/**
* Registers a placement to potentially show a paywall.
* The decision to show a paywall is determined by campaign rules and user assignments on the Superwall dashboard.
* @param placement - The ID of the placement to register.
* @param params - Optional parameters to pass with the placement.
* @param handlerId - An optional identifier used to associate specific event handlers (e.g., from `usePlacement`). Defaults to "default".
* @returns A promise that resolves when the placement registration is complete.
*/
registerPlacement: (
placement: string,
params?: Record<string, any>,
handlerId?: string | null,
) => Promise<void>
/**
* Retrieves the presentation result for a given placement.
* This can be used to understand what would happen if a placement were to be registered, without actually registering it.
* @param placement - The ID of the placement.
* @param params - Optional parameters for the placement.
* @returns A promise that resolves with the presentation result.
*/
getPresentationResult: (placement: string, params?: Record<string, any>) => Promise<any>
/**
* Dismisses any currently presented Superwall paywall.
* @returns A promise that resolves when the dismissal is complete.
*/
dismiss: () => Promise<void>
/**
* Preloads all paywalls configured in your Superwall dashboard.
* @returns A promise that resolves when preloading is complete.
*/
preloadAllPaywalls: () => Promise<void>
/**
* Preloads specific paywalls.
* @param placements - An array of placement IDs for which to preload paywalls.
* @returns A promise that resolves when preloading is complete.
*/
preloadPaywalls: (placements: string[]) => Promise<void>
/**
* Sets custom attributes for the current user.
* @param attrs - An object containing the attributes to set.
* @returns A promise that resolves when attributes are set.
*/
setUserAttributes: (attrs: Record<string, any>) => Promise<void>
/**
* Retrieves the current user's attributes.
* @returns A promise that resolves with the user's attributes.
*/
getUserAttributes: () => Promise<Record<string, any>>
/**
* Sets the logging level for the Superwall SDK.
* @param level - The desired log level (e.g., "debug", "info", "warn", "error", "none").
* @returns A promise that resolves when the log level is set.
*/
setLogLevel: (level: string) => Promise<void>
/**
* Sets the locale identifier for the Superwall SDK.
* This determines the language used when presenting paywalls.
* Can be changed at runtime without needing to reconfigure.
* @param localeIdentifier - The locale identifier (e.g., "en", "es", "fr"), or `null` to reset to the device locale.
* @returns A promise that resolves when the locale identifier is set.
*/
setLocaleIdentifier: (localeIdentifier: string | null) => Promise<void>
/**
* Sets attributes for third-party integrations.
* @param attributes - Object mapping IntegrationAttribute string values to their IDs
* @returns A promise that resolves when attributes are set
*/
setIntegrationAttributes: (attributes: IntegrationAttributes) => Promise<void>
/**
* Gets the currently set integration attributes.
* @returns A promise that resolves with the current integration attributes
*/
getIntegrationAttributes: () => Promise<Record<string, string>>
/* -------------------- Listener helpers -------------------- */
/**
* Initializes native event listeners for the SDK.
* This is typically called internally by the `SuperwallProvider`.
* @returns A cleanup function to remove the listeners.
* @internal
*/
_initListeners: () => () => void
setSubscriptionStatus: (status: SubscriptionStatus) => Promise<void>
getDeviceAttributes: () => Promise<Record<string, any>>
/**
* Retrieves the user's entitlements from Superwall's servers.
* This includes both active and inactive entitlements.
* @returns A promise that resolves with the entitlements information.
*/
getEntitlements: () => Promise<EntitlementsInfo>
}
/**
* @category Store
* @since 0.0.15
* Zustand store for Superwall SDK state and actions.
* @internal
*/
export const useSuperwallStore = create<SuperwallStore>((set, get) => ({
/* -------------------- State -------------------- */
isConfigured: false,
isLoading: false,
listenersInitialized: false,
configurationError: null,
user: null,
subscriptionStatus: {
status: "UNKNOWN",
},
/* -------------------- Actions -------------------- */
configure: async (apiKey, options) => {
set({ isLoading: true, configurationError: null })
try {
const { manualPurchaseManagement, manualPurchaseManagment, ...restOptions } = options || {}
// Support both spellings for backward compatibility
const isManualPurchaseManagement =
manualPurchaseManagement ?? manualPurchaseManagment ?? false
// Deep merge partial options with defaults to ensure all required fields are present
const mergedOptions = {
...DefaultSuperwallOptions,
...restOptions,
paywalls: {
...DefaultSuperwallOptions.paywalls,
...restOptions.paywalls,
restoreFailed: {
...DefaultSuperwallOptions.paywalls.restoreFailed,
...restOptions.paywalls?.restoreFailed,
},
},
logging: {
...DefaultSuperwallOptions.logging,
...restOptions.logging,
},
}
await SuperwallExpoModule.configure(
apiKey,
mergedOptions,
isManualPurchaseManagement,
pkg.version,
)
const currentUser = await SuperwallExpoModule.getUserAttributes()
const subscriptionStatus = await SuperwallExpoModule.getSubscriptionStatus()
set({
isConfigured: true,
isLoading: false,
configurationError: null,
user: currentUser as UserAttributes,
subscriptionStatus,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
set({
isLoading: false,
configurationError: errorMessage,
})
// Don't throw - let developers handle via state or callback
}
},
identify: async (userId, options) => {
await SuperwallExpoModule.identify(userId, options)
// TODO: Instead of setting users after identify, we should set this based on an event
await new Promise((resolve) => setTimeout(resolve, 0))
const [currentUser, subscriptionStatus] = await Promise.all([
SuperwallExpoModule.getUserAttributes(),
SuperwallExpoModule.getSubscriptionStatus(),
])
set({ user: currentUser as UserAttributes, subscriptionStatus })
},
reset: async () => {
await SuperwallExpoModule.reset()
const currentUser = await SuperwallExpoModule.getUserAttributes()
const subscriptionStatus = await SuperwallExpoModule.getSubscriptionStatus()
set({ user: currentUser as UserAttributes, subscriptionStatus })
},
registerPlacement: async (placement, params, handlerId = "default") => {
await SuperwallExpoModule.registerPlacement(placement, params, handlerId)
},
getPresentationResult: async (placement, params) => {
return SuperwallExpoModule.getPresentationResult(placement, params)
},
dismiss: async () => {
await SuperwallExpoModule.dismiss()
},
preloadAllPaywalls: async () => {
SuperwallExpoModule.preloadAllPaywalls()
},
preloadPaywalls: async (placements) => {
SuperwallExpoModule.preloadPaywalls(placements)
},
setUserAttributes: async (attrs) => {
await SuperwallExpoModule.setUserAttributes(attrs)
const currentUser = await SuperwallExpoModule.getUserAttributes()
set({ user: currentUser as UserAttributes })
},
getUserAttributes: async () => {
const attributes = await SuperwallExpoModule.getUserAttributes()
set({ user: attributes as UserAttributes })
return attributes
},
setLogLevel: async (level) => {
SuperwallExpoModule.setLogLevel(level)
},
setLocaleIdentifier: async (localeIdentifier) => {
SuperwallExpoModule.setLocaleIdentifier(localeIdentifier)
},
setIntegrationAttributes: async (attributes) => {
await SuperwallExpoModule.setIntegrationAttributes(attributes)
const currentUser = await SuperwallExpoModule.getUserAttributes()
set({ user: currentUser as UserAttributes })
},
getIntegrationAttributes: async () => {
return SuperwallExpoModule.getIntegrationAttributes()
},
setSubscriptionStatus: async (status) => {
await SuperwallExpoModule.setSubscriptionStatus(status)
},
getDeviceAttributes: async () => {
const attributes = await SuperwallExpoModule.getDeviceAttributes()
return attributes
},
getEntitlements: async () => {
const entitlements = await SuperwallExpoModule.getEntitlements()
return entitlements as EntitlementsInfo
},
/* -------------------- Listener helpers -------------------- */
_initListeners: (): (() => void) => {
// Use get() to read the state from within the store
if (get().listenersInitialized) {
console.warn("[Superwall] Listeners already initialized. Skipping.")
return () => {} // Return no-op cleanup
}
const subscriptions: { remove: () => void }[] = []
subscriptions.push(
SuperwallExpoModule.addListener(
"subscriptionStatusDidChange",
({ to }: { to: SubscriptionStatus }) => {
set({ subscriptionStatus: to })
},
),
)
// Listen for configuration events
subscriptions.push(
SuperwallExpoModule.addListener("handleSuperwallEvent", ({ eventInfo }) => {
if (eventInfo.event.event === "configFail") {
set({
configurationError: "Failed to load Superwall configuration",
isLoading: false,
})
} else if (eventInfo.event.event === "configRefresh") {
// Clear any previous errors on successful refresh
set({ configurationError: null })
}
}),
)
set({ listenersInitialized: true })
console.log("[Superwall] Initialized listeners", subscriptions.length)
return (): void => {
console.log("[Superwall] Cleaning up listeners", subscriptions.length)
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach is used for side effects only
subscriptions.forEach((s) => s.remove())
// Reset the state on cleanup
set({ listenersInitialized: false })
}
},
}))
/**
* @category Store
* @since 0.0.15
* Public interface for the Superwall store, excluding internal methods.
*/
export type PublicSuperwallStore = Omit<SuperwallStore, "configure" | "reset" | "_initListeners">
export const SuperwallContext = createContext<boolean>(false)
/**
* @category Hooks
* @since 0.0.15
* Core React hook for interacting with the Superwall SDK.
*
* This hook provides access to the Superwall store, which includes SDK state
* (like configuration status, user information, subscription status) and actions
* (like `identify`, `reset`, `registerPlacement`).
*
* It must be used within a component that is a descendant of `<SuperwallProvider />`.
*
* @template T - Optional type parameter for the selected state. Defaults to the entire `PublicSuperwallStore`.
* @param selector - An optional function to select a specific slice of the store's state.
* This is useful for performance optimization, as components will only re-render
* if the selected part of the state changes. Uses shallow equality checking
* via `zustand/shallow`. If omitted, the entire store is returned.
* @returns The selected slice of the Superwall store state, or the entire store if no selector is provided.
* @throws Error if used outside of a `SuperwallProvider`.
*
* @example
* // Get the entire store
* const superwall = useSuperwall();
* console.log(superwall.isConfigured);
* superwall.identify("user_123");
*
* @example
* // Select specific state properties
* const { user, subscriptionStatus } = useSuperwall(state => ({
* user: state.user,
* subscriptionStatus: state.subscriptionStatus,
* }));
* console.log(user?.appUserId, subscriptionStatus?.status);
*/
export function useSuperwall<T = PublicSuperwallStore>(selector?: (state: SuperwallStore) => T): T {
const inProvider = useContext(SuperwallContext)
if (!inProvider) {
throw new Error("useSuperwall must be used within a SuperwallProvider")
}
const identity = (state: SuperwallStore) => state as unknown as T
// biome-ignore lint/correctness/useHookAtTopLevel: good here
return useSuperwallStore(selector ? useShallow(selector) : identity)
}