Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,19 @@ EXPO_PUBLIC_APP_ENV=development
# Get a project ID at https://cloud.walletconnect.com
# EXPO_PUBLIC_WALLET_CONNECT_PROJECT_ID=

# Embedded wallets (Privy, optional)
# Enable Email and Google login plus Ethereum embedded wallets in the Privy
# dashboard. Both public values are safe to ship; never put a Privy secret here.
# ─── Embedded Wallets — Privy (optional) ──────────────────────────────
# Enables social/email login with automatic wallet creation for non-crypto
# native users. When both values are set, the onboarding screen shows a
# "Get started without a wallet" option backed by Privy's MPC embedded wallets.
#
# Setup:
# 1. Create a Privy app at https://dashboard.privy.io
# 2. Enable "Email" and "Google" login methods
# 3. Enable "Embedded Wallets → Ethereum" with createOnLogin = users-without-wallets
# 4. Copy the App ID and Client ID below
#
# Both values are public (safe to ship in the app binary). Never put a Privy
# secret key here. See docs/embedded-wallet-provider.md for the full evaluation.
# EXPO_PUBLIC_PRIVY_APP_ID=
# EXPO_PUBLIC_PRIVY_CLIENT_ID=

Expand Down
50 changes: 49 additions & 1 deletion .maestro/08-embedded-wallet-entry.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,67 @@
appId: xyz.guildpass.mobile
---
# E2E Test: Embedded wallet entry point
# E2E Test: Embedded wallet onboarding flow
#
# Run this flow in a build configured with EXPO_PUBLIC_PRIVY_APP_ID and
# EXPO_PUBLIC_PRIVY_CLIENT_ID. OTP completion is intentionally not automated:
# it requires a real email inbox and verifies the provider-hosted flow.
#
# CI note: For automated CI runs, use a Privy test-mode app ID that returns
# deterministic OTP codes, or mock the Privy SDK at the provider level.

- launchApp:
clearState: true

# ── Onboarding screen renders ─────────────────────────────────────────
- assertVisible:
id: "onboarding-screen"
- assertVisible:
id: "onboarding-embedded-wallet-button"

# ── Tap into embedded wallet flow ──────────────────────────────────────
- tapOn:
id: "onboarding-embedded-wallet-button"
- assertVisible:
id: "embedded-wallet-onboarding"

# ── Custody disclosure is visible before sign-in ───────────────────────
- assertVisible:
id: "custody-disclosure"
- assertVisible:
text: "About your embedded wallet"

# ── Expand disclosure and verify detail content ────────────────────────
- tapOn:
id: "custody-disclosure-toggle"
- assertVisible:
id: "custody-disclosure-details"
- assertVisible:
text: "How your keys are secured"
- assertVisible:
text: "Trade-offs vs. self-custody"
- assertVisible:
id: "custody-disclosure-learn-more"

# ── Collapse disclosure ────────────────────────────────────────────────
- tapOn:
id: "custody-disclosure-toggle"
- assertNotVisible:
id: "custody-disclosure-details"

# ── Sign-in UI elements are present ───────────────────────────────────
- assertVisible:
id: "social-google-button"
- assertVisible:
id: "embedded-wallet-email-input"
- assertVisible:
id: "embedded-wallet-email-button"

# ── "I have a wallet" back button returns to onboarding choice ─────────
- assertVisible:
id: "embedded-wallet-back-button"
- tapOn:
id: "embedded-wallet-back-button"
- assertVisible:
id: "onboarding-embedded-wallet-button"
- assertVisible:
id: "onboarding-get-started-button"
32 changes: 32 additions & 0 deletions .maestro/15-embedded-wallet-disconnect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
appId: xyz.guildpass.mobile
---
# E2E Test: Embedded wallet disconnect flow
#
# Verifies that an embedded-wallet connection displays the correct connection
# label on the profile screen and that disconnect returns the user to onboarding.
#
# Prerequisites: This flow assumes the app is already connected with an embedded
# wallet. In CI, seed the wallet store directly via Maestro's runScript or use a
# Privy test-mode build that auto-completes login.
#
# NOTE: Full Privy SDK logout verification cannot be done in E2E without mocking
# the provider. This test verifies the UI contract: disconnect clears the wallet
# state and navigates back to onboarding.

# ── Verify embedded wallet is shown on profile ─────────────────────────
- assertVisible:
id: "profile-screen"
- assertVisible:
id: "profile-connected"
- assertVisible:
text: "Embedded Wallet"
- assertVisible:
id: "connected-wallet-address"

# ── Disconnect ─────────────────────────────────────────────────────────
- tapOn:
id: "wallet-disconnect-button"

# ── Verify return to wallet connect form ───────────────────────────────
- assertVisible:
id: "wallet-connect-form"
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The application is built on a robust, feature-driven foundation designed for lon
| `app/` | Expo Router file-based pages and layouts |
| `app/access-scanner.tsx` | QR code scanner screen using expo-camera |
| `src/features/access/` | Access check hooks and QR payload validation |
| `src/features/wallet/` | Wallet connectors, embedded wallet (Privy), stores |
| `src/` | Feature modules, hooks, stores, and services |
| `docs/` | Architecture and integration guides |
| `tests/` | Vitest unit tests |
Expand Down Expand Up @@ -252,7 +253,7 @@ Deep links work when the app is cold-started (not already running). The app will
## 🗺️ Roadmap

- [ ] **Native Wallet Integration**: Support for WalletConnect, MetaMask, and Coinbase Wallet.
- [ ] **Smart Onboarding**: Social login and embedded wallets for non-crypto native users.
- [x] **Smart Onboarding**: Social login and embedded wallets for non-crypto native users.
- [x] **Push Notifications**: Real-time alerts for role updates and access grants.
- [x] **QR Access Verification**: Scan GuildPass QR codes to verify token-gated resource access from the mobile app.
- [ ] **Offline Resilience**: Advanced caching layer for viewing memberships without connectivity.
Expand Down
23 changes: 23 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ declared in `src/lib/`, so the fan-out is readable, ordered, and awaitable in on
Adding a cross-feature transition means adding it to one of these modules, not importing
another feature's store into a hook or component.

### Wallet providers

Three wallet paths are supported, all converging through the `WalletConnector` interface
into `useWalletStore`:

| Path | Provider | Connection kind | How it works |
|---|---|---|---|
| **Manual entry** | None | `manual` | User pastes an EVM address; `createManualConnector` wraps it |
| **WalletConnect** | WalletConnect v2 | `walletconnect` | WC modal → EIP-1193 → `createWalletConnectConnector` |
| **Embedded wallet** | Privy (`@privy-io/expo`) | `embedded` | Email OTP or Google OAuth → Privy provisions MPC wallet → `createEmbeddedConnector` wraps the address |

**Key design principle:** Privy is only the provisioning layer. Once the embedded wallet
address enters `useWalletStore`, every downstream flow (memberships, guilds, access checks,
sync, attestations) sees a standard EVM address. No screen or hook needs to know the wallet
was provisioned by Privy.

The embedded path is feature-flagged via `EXPO_PUBLIC_PRIVY_APP_ID` and
`EXPO_PUBLIC_PRIVY_CLIENT_ID` environment variables. When both are set,
`isEmbeddedWalletEnabled` is `true` and the onboarding screen offers the social/email option.

See `docs/embedded-wallet-provider.md` for the provider evaluation, security model, and
custody trade-off documentation.

### Selectors

Subscribe with a selector, never by calling the store hook bare. `useWalletStore()`
Expand Down
131 changes: 131 additions & 0 deletions docs/embedded-wallet-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Embedded Wallet Provider Evaluation

> **Decision:** Privy (`@privy-io/expo`)
> **Date:** 2026-07-27
> **Status:** Adopted — integrated into `src/features/wallet/`

## Evaluation Criteria

| Criterion | Weight | Notes |
|---|---|---|
| React Native / Expo SDK maturity | Critical | Must have first-party Expo support; no bare-RN-only workarounds |
| Social login providers | High | Email + Google required; Apple required for App Store |
| Wallet provisioning model | High | Automatic on first login; no user ceremony |
| Custody / key security model | High | Must NOT be fully custodial; MPC or TEE preferred |
| Chain support | Medium | All EVM chains (GuildPass is EVM-only) |
| Cost at launch scale | Medium | Free tier or low-cost for <1000 MAU |
| Wallet export | Medium | Users should be able to extract their key if desired |

---

## Providers Evaluated

### Privy ✅ (Selected)

| Criterion | Assessment |
|---|---|
| **SDK** | `@privy-io/expo` — first-party, actively maintained, Expo-native. Already in `package.json` |
| **Social login** | Email OTP, Google, Apple, Twitter, Discord, GitHub, Farcaster |
| **Wallet provisioning** | `createOnLogin: "users-without-wallets"` — zero-ceremony Ethereum wallet creation |
| **Custody model** | **MPC 2-of-3 key splitting**: device share (Secure Enclave), Privy infra share (HSM-backed), recovery share (encrypted to user's auth factor). No single party ever holds the full private key |
| **Chain support** | All EVM chains |
| **Cost** | Free: 1,000 MAU. Growth plan: usage-based pricing |
| **Wallet export** | Supported — users can extract their full private key through the SDK |
| **Recovery** | Re-authenticate with same email/social account on any device |

**Why Privy:**
1. Only provider with a first-party, production-ready Expo SDK (`@privy-io/expo`)
2. MPC key model is the gold standard for embedded wallets — no seed phrase, no full custodial risk
3. `createOnLogin` config eliminates all wallet ceremony
4. Already integrated and partially wired in the codebase
5. Auth-factor-based recovery means users can't lose access by losing a device

### Web3Auth ❌

| Criterion | Assessment |
|---|---|
| **SDK** | `@web3auth/react-native-sdk` — available but heavier; requires additional native module configuration that conflicts with Expo managed workflow |
| **Social login** | Broad provider support |
| **Custody model** | Threshold key splitting (similar to MPC) |
| **Why not** | Heavier SDK footprint, more complex configuration, less Expo-native than Privy. Would require `expo prebuild` for native module linking |

### Magic (now Magic.link) ❌

| Criterion | Assessment |
|---|---|
| **SDK** | `@magic-sdk/react-native` — deprecated React Native SDK, limited maintenance |
| **Why not** | Deprecated RN SDK with no Expo-specific support. Would require significant custom bridging |

### Turnkey ❌

| Criterion | Assessment |
|---|---|
| **SDK** | Lower-level API; no React Native SDK. Requires building custom auth + wallet UI |
| **Why not** | Too low-level for a mobile app. Would require building the entire auth flow, wallet UI, and key management from scratch |

### Particle Network ❌

| Criterion | Assessment |
|---|---|
| **SDK** | `@particle-network/rn-auth` — available but less mature Expo support |
| **Why not** | Less mature Expo support compared to Privy. Smaller developer community |

---

## Security Model

### What Privy Protects Against

- **Seed phrase loss/theft**: No seed phrase exists. Key shares are distributed across device, Privy infrastructure, and an auth-factor-encrypted recovery share.
- **Device loss**: User re-authenticates with their email/social account on a new device to reconstruct their key.
- **Single point of compromise**: MPC ensures no single party (not Privy, not the device) ever holds the full private key.

### What Privy Does NOT Protect Against

- **Privy infrastructure compromise**: If Privy's HSM-backed infrastructure is breached AND the attacker obtains the device share, the key could be reconstructed. This is a fundamental trust trade-off of any embedded wallet provider.
- **Social account takeover + device theft**: An attacker who compromises both the user's auth factor (email/Google account) AND their device could potentially reconstruct the key.
- **Nation-state actors**: Out of scope, consistent with the project's [threat model](./threat-model.md).

### Alignment with GuildPass Threat Model

The existing [threat model](./threat-model.md) already documents "Future: embedded private keys" as a **Critical** asset with planned storage in `expo-secure-store` / Secure Enclave. Privy's device share leverages exactly this: the device key share is stored in the platform's hardware-backed secure enclave (iOS Keychain / Android Keystore), aligned with the existing security architecture.

---

## Integration Architecture

```
┌─────────────────────────────────────────────────────┐
│ Onboarding Screen │
│ ┌─────────────────────────────────────────────────┐│
│ │ CustodyDisclosure (trade-off explanation) ││
│ │ Email OTP / Google OAuth sign-in ││
│ └─────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐│
│ │ Privy SDK (useLoginWithEmail / useLoginWithOAuth)│
│ │ → auto-creates embedded Ethereum wallet ││
│ │ → returns EVM address ││
│ └─────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐│
│ │ createEmbeddedConnector(address) ││
│ │ → same WalletConnector interface as manual/WC ││
│ └─────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐│
│ │ useWalletStore ││
│ │ walletAddress = "0x...", connectionKind = "embedded" │
│ └─────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ All downstream flows (memberships, guilds, access │
│ checks, sync, attestations) see a standard EVM │
│ address — completely provider-agnostic. │
└─────────────────────────────────────────────────────┘
```

Key design principle: **Privy is only the provisioning layer.** Once the embedded wallet address enters `useWalletStore`, every downstream flow treats it identically to a manually-entered or WalletConnect address. No screen, hook, or service needs to know the wallet was provisioned by Privy.
Loading