GameCraftGameCraft

Credits & Referral System

User credits and referral program for ProductReady

Credits & Referral System

ProductReady includes a built-in credits system and referral program to incentivize user growth and engagement.

Credits System

Credits are a virtual currency that users can earn and use within the platform. Credits are stored in cents (e.g., 1000 = $10.00).

Credit Sources

Users can earn credits through various activities:

SourceDescription
referralInviting friends to sign up
onboardingCompleting onboarding steps
early_adopterEarly adopter rewards
promotionVarious promotional campaigns
manualAdmin-granted credits
redemptionRedeeming promotional codes

Database Schema

// User credits balance
export const userCredits = pgTable("user_credits", {
  userId: text("user_id").primaryKey(),
  balance: integer("balance").default(0).notNull(),
  totalEarned: integer("total_earned").default(0).notNull(),
  totalUsed: integer("total_used").default(0).notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at"),
});

// Transaction history
export const userCreditTransactions = pgTable("user_credit_transactions", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull(),
  amount: integer("amount").notNull(),
  balanceAfter: integer("balance_after").notNull(),
  source: text("source").notNull(),
  referenceId: text("reference_id"),
  description: text("description"),
  grantedBy: text("granted_by"),
  spaceId: text("space_id"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

API Endpoints

Get My Credits

// Get current user's credit balance
const credits = await trpc.credits.getMyCredits.query();
// Returns: { userId, balance, totalEarned, totalUsed, balanceFormatted }

Get My Transactions

// Get transaction history
const { transactions, total } = await trpc.credits.getMyTransactions.query({
  limit: 20,
  offset: 0,
  source: "referral", // optional filter
});

Admin Endpoints

System admins can manage user credits:

// Add credits to a user
await trpc.credits.addCredits.mutate({
  userId: "user_123",
  amount: 500, // $5.00
  source: "manual",
  description: "Customer support credit",
});

// List all transactions
const { transactions, total } = await trpc.credits.listTransactions.query({
  userId: "user_123", // optional
  limit: 50,
});

// List users with credits
const { users, total } = await trpc.credits.listUsersWithCredits.query({
  limit: 20,
});

Referral Program

The referral program allows users to invite friends and earn credits when they sign up.

How It Works

  1. User generates a unique referral code
  2. User shares their referral link: https://yourapp.com/sign-up?ref=CODE
  3. New user signs up using the link
  4. Both users receive credits:
    • Referrer: $5.00 in credits
    • New User: $2.00 in credits

Database Schema

Referral fields are added to user_profiles:

export const userProfiles = pgTable("user_profiles", {
  // ... other fields
  referralCode: text("referral_code").unique(),
  referredBy: text("referred_by"),
  referralRewarded: timestamp("referral_rewarded"),
});

API Endpoints

Get Referral Info

const info = await trpc.referral.getMyReferralInfo.query();
// Returns: { referralCode, referralCount, totalEarned }

Generate Referral Code

const { referralCode } = await trpc.referral.generateReferralCode.mutate();

Validate Referral Code (Public)

const { valid, referrerName } = await trpc.referral.validateReferralCode.query({
  code: "ABC123",
});

UI Components

The credits and referral UI is integrated into the Account Settings modal:

import { AccountSettingsModal } from "~/components/dashboard/account-settings-modal";

// The modal includes tabs for:
// - Profile
// - Credits (balance + transactions)
// - Referral (link + stats)
// - API Keys

Sign-Up Flow

The sign-up page automatically handles referral codes:

  1. Reads ?ref=CODE from URL
  2. Validates the code and shows referrer name
  3. Stores code in sessionStorage for OAuth flow
  4. After registration, processes referral in dashboard
// In sign-up page
const refCode = searchParams.get("ref");
const { data: refValidation } = trpc.referral.validateReferralCode.useQuery(
  { code: refCode || "" },
  { enabled: !!refCode }
);

Customization

You can customize reward amounts in src/server/routers/referral.ts:

const REFERRER_REWARD = 500; // $5.00 for the referrer
const REFERRED_REWARD = 200; // $2.00 for the new user

Best Practices

  1. Fraud Prevention: Consider adding:

    • Email verification before granting referral rewards
    • IP/device tracking to prevent self-referrals
    • Rate limiting on referral code generation
  2. Credit Expiration: Implement expiration if needed:

    • Add expiresAt field to credits
    • Run periodic cleanup jobs
  3. Audit Trail: All credit changes are logged in userCreditTransactions for compliance and debugging.

Onboarding System

ProductReady includes a user onboarding system to guide new users through key features. The onboarding state is stored in user_profiles.onboarding (JSONB).

Onboarding State Structure

interface OnboardingState {
  /** Whether user dismissed referral prompt */
  referral?: boolean;
  gettingStarted: {
    /** Whether user dismissed the Getting Started stepper */
    dismissed: boolean;
    steps: {
      createPost: { done: boolean; doneAt: string | null };
      managePosts: { done: boolean; doneAt: string | null };
      shareReferral: { done: boolean; doneAt: string | null };
    };
  };
}

Components

Getting Started Stepper

A dismissible progress card shown on the dashboard:

import { GettingStartedStepper } from "~/components/dashboard/getting-started-stepper";

<GettingStartedStepper />

Features:

  • 3-step progress tracker (Create Post → Manage Posts → Share Referral)
  • Auto-completes steps based on user actions
  • Can be manually dismissed by user
  • Persists state to database

Referral Prompt

A sidebar card encouraging users to invite friends:

import { ReferralPrompt } from "~/components/dashboard/referral-prompt";

<ReferralPrompt onInviteClick={() => setShowReferral(true)} />

Features:

  • Only shows if user hasn't invited anyone (onboarding.referral !== true)
  • Dismissible with persistence to database
  • Auto-hides when user successfully invites someone

API Endpoints

Get Onboarding State

const state = await trpc.users.getOnboarding.query();
// Returns: { referral?: boolean, gettingStarted: {...} }

Update Onboarding State

// Mark referral prompt as dismissed
await trpc.users.updateOnboarding.mutate({
  onboarding: { referral: true },
});

Getting Started Stepper API

// Get stepper state (onboarding router)
const state = await trpc.onboarding.getState.query();

// Complete a step
await trpc.onboarding.completeStep.mutate({ step: "createPost" });

// Dismiss the stepper
await trpc.onboarding.dismiss.mutate();

Auto-Complete Hooks

Automatically mark steps as complete when user visits certain pages:

// In dashboard page
useOnboardingAutoComplete("createPost");

// In posts list page
useOnboardingAutoComplete("managePosts");

// In referral settings
useOnboardingStepComplete("shareReferral", () => {
  // Triggered when user copies referral link
});

Customization

To add new onboarding steps:

  1. Update OnboardingState in src/db/schema/user-profiles.ts
  2. Add step definition to STEPS array in getting-started-stepper.tsx
  3. Add step key to OnboardingStepSchema in src/server/routers/onboarding.ts
  4. Create auto-complete hook in relevant pages

Important Fix

Referral Prompt Dismissal: When user clicks the close button on ReferralPrompt, the state must be persisted to database, not just local state:

// ✅ Correct: Persist to database
const handleDismiss = () => {
  updateOnboardingMutation.mutate({
    onboarding: { referral: true },
  });
};

// ❌ Wrong: Only local state (bug fixed in 2025-12-18)
const handleDismiss = () => {
  setDismissed(true); // Lost on page refresh!
};

On this page