Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
076ef6d
feat(privy): one-liner identifyPrivyUser to cluster a Privy user's wa…
claude Jul 13, 2026
5d39f80
refactor(privy): address API review — safer setActive contract, smart…
claude Jul 13, 2026
b937acf
refactor(privy): drop core identify() API changes; solve attribution …
claude Jul 14, 2026
7bdc05b
feat(privy): add identify(user, { privy: true }); drop the React hook
claude Jul 14, 2026
b3f8ad1
feat(privy): default attribution to user.wallet; make activeAddress o…
claude Jul 14, 2026
ed0006a
fix(privy): address Codex review — 4 correctness fixes
claude Jul 17, 2026
dbd5d3c
fix(privy): preserve current address when active wallet isn't linked
claude Jul 17, 2026
ab05adb
refactor(privy): root fix — clustering identifies no longer mutate ac…
claude Jul 17, 2026
5c33237
docs(privy): add integration plan/status doc; fix stale activeAddress…
claude Jul 17, 2026
2581432
fix(privy): reconcile chain before emitting; disambiguate dedup keys
claude Jul 23, 2026
594dc71
fix(privy): size-bound the identify dedup store for many-wallet users
claude Jul 23, 2026
1aab641
fix(privy): direct identifyPrivyUser() preserves the connected wallet
claude Jul 23, 2026
b026ba6
fix(privy): carry the DID on every clustering identify; skip sync whe…
claude Jul 23, 2026
6fe2370
docs(privy): fix stale attribution docs; drop dead export
claude Jul 24, 2026
e8712bf
Merge remote-tracking branch 'origin/main' into claude/privy-identify…
claude Jul 25, 2026
bff1f02
Merge remote-tracking branch 'origin/main' into claude/privy-identify…
yosriady Aug 4, 2026
b9fc80b
feat(privy): parse all linked accounts; make identify dedup profile-a…
yosriady Aug 4, 2026
bc93e98
fix: repair broken pnpm-lock.yaml duplicated mapping keys
yosriady Aug 4, 2026
7c046de
fix(privy): address review findings on dedup, cookie budget, and docs
yosriady Aug 4, 2026
de9256c
docs(privy): replace the plan doc with how-it-works; add successive-l…
yosriady Aug 5, 2026
4292433
feat(privy): drop the { privy: true } flag; dispatch on shape
yosriady Aug 5, 2026
048badf
docs(privy): cover apps with both Privy and non-Privy users
yosriady Aug 5, 2026
68381c7
docs(privy): remove em dashes from prose and comments
yosriady Aug 5, 2026
cd19cd9
docs(privy): consolidate the two Privy docs into one
yosriady Aug 5, 2026
711fd22
fix: don't burn the identify dedup entry on an excluded chain
yosriady Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ You can install Formo on:

Visit Formo's [Developer Docs](https://docs.formo.so) for detailed guides on local testing, debugging, and consent management.

Using [Privy](./docs/PRIVY_INTEGRATION.md)? `identify(user)`
clusters all of a Privy user's linked wallets under a single identity.

## Methodology

Learn how Formo handles [onchain attribution](https://docs.formo.so/data/attribution) and [data collection](https://docs.formo.so/data/what-we-collect).
Expand Down
494 changes: 494 additions & 0 deletions docs/PRIVY_INTEGRATION.md

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

296 changes: 246 additions & 50 deletions src/FormoAnalytics.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export * from "./FormoAnalytics";
export * from "./types";
export { formofy } from "./initialization";

export { parsePrivyProperties } from "./privy";
export { parsePrivyProperties, identifyPrivyUser } from "./privy";
export type {
IdentifyPrivyUserOptions,
PrivyUser,
PrivyLinkedAccount,
PrivyAccountType,
Expand Down
14 changes: 12 additions & 2 deletions src/event/EventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ class EventFactory implements IEventFactory {
/**
* Returns the document referrer with same-host referrers filtered out.
* Internal navigation populates `document.referrer` with the previous page
* on the same site, which is not an attribution signal treating it as
* on the same site, which is not an attribution signal - treating it as
* "external" would otherwise let an internal URL become the session's
* first-touch referrer after a direct landing.
*/
Expand Down Expand Up @@ -808,7 +808,17 @@ class EventFactory implements IEventFactory {
const chainId = 'chainId' in event ? (event.chainId as ChainID) : undefined;
formoEvent.address = this.validateEventAddress(address, chainId);
}
formoEvent.user_id = userId || null;
// An identify event asserts an explicit identity in its own payload (e.g. a
// Privy DID for each wallet being clustered). Keep that payload user_id
// rather than overwriting it with the active-session user id - otherwise a
// clustering identify that intentionally leaves the active user unchanged
// (setActive:false) would be stripped of its DID, defeating server-side
// wallet clustering. Fall back to the active-session user id when the
// identify payload carries none; all other events use the session user id.
formoEvent.user_id =
event.type === "identify"
? formoEvent.user_id ?? userId ?? null
: userId || null;
Comment on lines +818 to +821

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize empty identify user IDs

When an app passes an empty string from an optional user-id variable, this new nullish fallback preserves formoEvent.user_id === "" for identify events instead of treating it as absent like the previous userId || null path and the identify() state update (if (userId)) still do. In that scenario identify payloads can be sent with a shared empty user_id, so keep falling back on falsy/empty payload user IDs while still preserving real Privy DIDs.

Useful? React with 👍 / 👎.


return formoEvent as IFormoEvent;
}
Expand Down
11 changes: 8 additions & 3 deletions src/privy/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
/**
* Privy integration module
*
* Provides utilities for enriching wallet profiles with Privy user data.
* This module exports the property extraction utility and related types.
* Provides utilities for enriching wallet profiles with Privy user data:
* `parsePrivyProperties` (low-level parsing) and `identifyPrivyUser` (identify
* every linked wallet under the user's DID). The same behavior is also
* available as `formo.identify(user, { privy: true })`.
*
* This module is React-free so it can be used from the `core` entry.
*/

export { parsePrivyProperties } from "./utils";
export { parsePrivyProperties, identifyPrivyUser } from "./utils";
export type { IdentifyPrivyUserOptions } from "./utils";
export type {
PrivyUser,
PrivyLinkedAccount,
Expand Down
25 changes: 23 additions & 2 deletions src/privy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ export interface PrivyLinkedAccount {
firstName?: string | null;
lastName?: string | null;

// Passkey-specific fields
credentialId?: string | null;

// Custom-auth-specific fields
customUserId?: string | null;

// Cross-app-specific fields. A cross_app account has no top-level `address`;
// its wallets live in these arrays (e.g. Abstract Global Wallet).
embeddedWallets?: Array<{ address: string; chainType?: string | null }> | null;
smartWallets?: Array<{ address: string; chainType?: string | null }> | null;

// Verification timestamps
firstVerifiedAt?: Date | null;
latestVerifiedAt?: Date | null;
Expand All @@ -91,8 +102,12 @@ export interface PrivyUser {
/** Privy user ID in DID format (e.g., "did:privy:cm3np...") */
id: string;

/** Account creation timestamp */
createdAt?: Date;
/**
* Account creation timestamp. Privy's React SDK supplies a `Date`, but a user
* object from the REST API or one that made a JSON round-trip carries an ISO
* string or epoch number, so all three are accepted and normalized.
*/
createdAt?: Date | string | number;

/** All linked accounts */
linkedAccounts?: PrivyLinkedAccount[];
Expand Down Expand Up @@ -132,6 +147,8 @@ export interface PrivyUser {
instagram?: { subject: string; username: string | null };
spotify?: { subject: string; email: string | null; name: string | null };
tiktok?: { subject: string; username: string | null; name: string | null };
/** Privy's Twitch account exposes only subject/username - no email. */
twitch?: { subject: string; username: string | null };
line?: { subject: string; name: string | null; email: string | null };
telegram?: {
telegramUserId: string;
Expand Down Expand Up @@ -159,9 +176,11 @@ export interface PrivyProfileProperties {
privyDid: string;
privyCreatedAt?: number;
email?: string;
phone?: string;
apple?: string;
discord?: string;
twitter?: string;
twitch?: string;
farcaster?: string;
github?: string;
google?: string;
Expand All @@ -171,6 +190,8 @@ export interface PrivyProfileProperties {
telegram?: string;
tiktok?: string;
instagram?: string;
/** The `customUserId` of a linked `custom_auth` account, when present. */
customUserId?: string;
[key: string]: unknown;
}

Expand Down
Loading
Loading