User Management
User management in Superwall involves identifying users, tracking their attributes, and managing their subscription status across app sessions.
User Identity System
Superwall uses a dual-identity system:
┌──────────────────────────────────────────┐
│ User Identity │
├──────────────────────────────────────────┤
│ aliasId │ Auto-generated by SDK │
│ │ Persists across sessions │
│ │ Device-specific │
├───────────────┼──────────────────────────┤
│ appUserId │ Your user ID │
│ │ Set via identify() │
│ │ Links to your backend │
└──────────────────────────────────────────┘
aliasId
- Automatically generated when SDK initializes
- Device-specific identifier
- Persists across app restarts
- Used for anonymous users, experiment assignments
appUserId
- Your user ID from your authentication system
- Set explicitly via
identify()
- Links device to user account
- Used for cross-device tracking, user-specific campaigns
The useUser Hook
The useUser hook provides user management functionality:
import { useUser } from "expo-superwall";
function UserProfile() {
const {
identify,
update,
signOut,
refresh,
user,
subscriptionStatus,
} = useUser();
return (
<View>
<Text>User ID: {user?.appUserId}</Text>
<Text>Status: {subscriptionStatus?.status}</Text>
</View>
);
}
Return Values
interface UseUserReturn {
// Actions
identify: (userId: string, options?: IdentifyOptions) => Promise<void>;
update: (attrs: Record<string, any> | ((old) => Record<string, any>)) => Promise<void>;
signOut: () => void;
refresh: () => Promise<Record<string, any>>;
setSubscriptionStatus: (status: SubscriptionStatus) => Promise<void>;
setIntegrationAttributes: (attrs: IntegrationAttributes) => Promise<void>;
getIntegrationAttributes: () => Promise<Record<string, string>>;
getEntitlements: () => Promise<EntitlementsInfo>;
// State
user?: UserAttributes | null;
subscriptionStatus?: SubscriptionStatus;
}
Identifying Users
Identify a user when they log in:
const { identify } = useUser();
const handleLogin = async (email: string, password: string) => {
// Your auth logic
const user = await authService.login(email, password);
// Identify with Superwall
await identify(user.id);
// Optionally set attributes immediately
await update({
email: user.email,
name: user.name,
tier: user.subscriptionTier,
});
};
IdentifyOptions
await identify(userId, {
restorePaywallAssignments: true, // Restore A/B test assignments
});
restorePaywallAssignments:
true - Restores user’s previous paywall variant assignments
false (default) - Assigns new variants
- Use when: Users frequently switch accounts or reinstall app
Restoring assignments ensures consistent A/B test experiences across devices and sessions.
User Attributes
UserAttributes Interface
interface UserAttributes {
// System attributes (read-only)
aliasId: string; // SDK-generated ID
appUserId: string; // Your user ID
applicationInstalledAt: string; // ISO date string
seed: number; // For experiment bucketing
// Custom attributes (read-write)
[key: string]: any | null;
}
Setting Attributes
const { update } = useUser();
// Set attributes directly
await update({
name: "John Doe",
email: "[email protected]",
age: 28,
isPremium: false,
lastActiveAt: new Date().toISOString(),
});
// Update based on previous values
await update((old) => ({
...old,
loginCount: (old.loginCount || 0) + 1,
lastLogin: Date.now(),
}));
Supported Value Types
| Type | Example | Notes |
|---|
| String | "John Doe" | Any string value |
| Number | 28, 3.14 | Integer or float |
| Boolean | true, false | For flags |
| Date | new Date() | Converted to ISO string |
| null | null | Removes attribute |
Arrays and nested objects are not supported and will be dropped.
Reserved Attributes
Attributes starting with $ are reserved for Superwall:
// These will be ignored
await update({
$customField: "value", // ❌ Dropped
customField: "value", // ✅ Accepted
});
Attribute Use Cases
Campaign Targeting
Use attributes in dashboard audience filters:
await update({
subscriptionTier: "pro",
totalPurchases: 5,
country: "US",
registeredDays: 30,
});
Dashboard Rule:
Show paywall if:
subscriptionTier != "pro" AND
totalPurchases < 3 AND
country == "US"
Personalization
Reference attributes in paywall content:
await update({
firstName: "Sarah",
favoriteFeature: "fishing",
});
Paywall Template:
<h1>Hey {{ user.firstName }}, upgrade to unlock {{ user.favoriteFeature }}!</h1>
Analytics Integration
const { update } = useUser();
useEffect(() => {
update({
amplitudeDeviceId: Analytics.getDeviceId(),
mixpanelDistinctId: Mixpanel.getDistinctId(),
});
}, []);
Refreshing User Data
Manually fetch fresh user attributes:
const { refresh, user } = useUser();
const syncUserData = async () => {
const freshAttrs = await refresh();
console.log("Updated attributes:", freshAttrs);
};
Refreshing:
- Fetches latest attributes from Superwall servers
- Updates subscription status
- Returns fresh attribute object
Signing Out
Reset user identity when logging out:
const { signOut } = useUser();
const handleLogout = async () => {
// Your logout logic
await authService.logout();
// Reset Superwall identity
signOut();
// SDK generates new aliasId
// appUserId is cleared
// Attributes are reset
};
What happens:
- Current user identity is cleared
- New
aliasId is generated
appUserId is reset
- Custom attributes are cleared
- Paywall assignments may change
Always call signOut() when users log out to ensure accurate analytics and targeting.
Integration Attributes
Link third-party attribution and analytics platform IDs:
const { setIntegrationAttributes, getIntegrationAttributes } = useUser();
// Set integration IDs
await setIntegrationAttributes({
adjustId: "adjust_123",
amplitudeUserId: "amp_456",
firebaseAppInstanceId: "firebase_789",
mixpanelDistinctId: "mp_abc",
});
// Retrieve them later
const integrations = await getIntegrationAttributes();
console.log(integrations.adjustId); // "adjust_123"
Supported Integrations
- Attribution: Adjust, AppsFlyer, Kochava, Tenjin
- Analytics: Amplitude, Mixpanel, Posthog, mParticle
- Marketing: Braze, Iterable, Customer.io, OneSignal, Airship
- Other: Firebase, Facebook, CleverTap
Full list:
type IntegrationAttribute =
| "adjustId"
| "amplitudeDeviceId"
| "amplitudeUserId"
| "appsflyerId"
| "brazeAliasName"
| "brazeAliasLabel"
| "onesignalId"
| "fbAnonId"
| "firebaseAppInstanceId"
| "iterableUserId"
| "mixpanelDistinctId"
| "mparticleId"
| "clevertapId"
| "airshipChannelId"
| "kochavaDeviceId"
| "tenjinId"
| "posthogUserId"
| "customerioId";
User Attributes vs Placement Params
| Aspect | User Attributes | Placement Params |
|---|
| Scope | User-level, persistent | Placement-specific, ephemeral |
| Lifetime | Across sessions | Single placement call |
| Use Case | User demographics, behavior | Contextual data |
| Set Via | update() | registerPlacement({ params }) |
Example:
// User attributes - persistent
await update({
subscriptionTier: "free",
country: "US",
});
// Placement params - contextual
await registerPlacement({
placement: "article_read",
params: {
articleId: "123",
category: "tech",
wordCount: 1500,
},
});
Dashboard can target on both:
Show paywall if:
user.subscriptionTier == "free" AND
params.category == "tech" AND
params.wordCount > 1000
Accessing User State
Reactive Access
const { user, subscriptionStatus } = useUser();
// Component re-renders when user or status changes
return (
<View>
<Text>Welcome, {user?.appUserId}</Text>
<Text>Status: {subscriptionStatus?.status}</Text>
</View>
);
Direct Access
const { getUserAttributes } = useSuperwall();
// Fetch without subscribing to updates
const attrs = await getUserAttributes();
console.log(attrs);
Common Patterns
Auto-Identify on App Start
function App() {
const { identify } = useUser();
useEffect(() => {
const initUser = async () => {
const userId = await AsyncStorage.getItem("userId");
if (userId) {
await identify(userId);
}
};
initUser();
}, []);
return <AppContent />;
}
Sync Attributes Periodically
function UserSync() {
const { update } = useUser();
useEffect(() => {
const syncAttributes = async () => {
const userData = await fetchUserProfile();
await update({
name: userData.name,
email: userData.email,
subscriptionTier: userData.tier,
});
};
// Sync on mount and every 5 minutes
syncAttributes();
const interval = setInterval(syncAttributes, 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
return null;
}
Track User Behavior
function FeatureUsageTracker() {
const { update } = useUser();
const trackFeatureUse = async (feature: string) => {
await update((old) => ({
...old,
[`${feature}UseCount`]: (old[`${feature}UseCount`] || 0) + 1,
lastUsedFeature: feature,
lastActiveAt: new Date().toISOString(),
}));
};
return { trackFeatureUse };
}
Compat SDK Equivalent
For those using the Compat SDK:
import Superwall from "expo-superwall/compat";
// Identify
await Superwall.shared.identify({
userId: "user_123",
options: { restorePaywallAssignments: true },
});
// Set attributes
await Superwall.shared.setUserAttributes({
name: "John Doe",
tier: "premium",
});
// Get attributes
const attrs = await Superwall.shared.getUserAttributes();
// Reset
await Superwall.shared.reset();
// Integration attributes
await Superwall.shared.setIntegrationAttributes({
adjustId: "adjust_123",
});
Best Practices
1. Identify Early
// ✅ Good - Identify as soon as you have userId
useEffect(() => {
if (isAuthenticated && userId) {
identify(userId);
}
}, [isAuthenticated, userId]);
// ❌ Bad - Waiting too long
// Don't wait until first paywall
2. Keep Attributes Updated
// ✅ Good - Update when data changes
useEffect(() => {
if (userProfile) {
update({
tier: userProfile.subscriptionTier,
lastSyncAt: Date.now(),
});
}
}, [userProfile]);
3. Don’t Store Sensitive Data
// ❌ Bad - Sensitive data
await update({
creditCardNumber: "1234...",
ssn: "123-45-6789",
});
// ✅ Good - Non-sensitive metadata
await update({
hasPaymentMethod: true,
accountCreatedAt: date.toISOString(),
});
4. Use Meaningful Names
// ✅ Good - Descriptive
await update({
subscriptionTier: "pro",
totalArticlesRead: 42,
preferredLanguage: "en",
});
// ❌ Bad - Cryptic
await update({
t: "pro",
c: 42,
l: "en",
});
Further Reading