npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@akin-travel/partner-sdk

v1.0.8

Published

SDK for third-party partners to integrate Akin auth and loyalty services

Downloads

264

Readme

@akin-online/partner-sdk

SDK for third-party partners to integrate Akin auth and loyalty services into their React applications.

Features

  • Authentication - Magic links (passwordless), passkeys (WebAuthn), Google OAuth
  • Loyalty - Tier display, points balance, transaction history, progress tracking
  • Tier Requirements - Flexible key/value qualification rules with display utilities
  • Headless Components - Full control over your UI with render props
  • TypeScript - Full type support
  • Phone Input - International phone number input with validation

Getting Access

The @akin-online/partner-sdk is a private npm package available to authorized Akin partners.

Partner Onboarding

Contact your Akin partner representative or email [email protected] to request access. You'll receive the following via secure channel:

| Credential | Purpose | |------------|---------| | npm access token | For installing the private @akin-online/partner-sdk package | | Partner ID | Your unique identifier in the Akin system | | API key | For authenticating with the Akin API | | SDK environment variables | Firebase/GIP credentials and API URLs (NEXT_PUBLIC_AKIN_SDK_*) |

Configure npm Access

Add the npm access token to your .npmrc file:

# ~/.npmrc or project .npmrc
//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN

Security Note: Never commit your .npmrc file with tokens to source control. Add it to .gitignore.

Installation

npm install @akin-online/partner-sdk

Peer Dependencies

npm install react-phone-number-input

Quick Start

1. Wrap your app with AkinProvider

// app/layout.tsx or _app.tsx
import { AkinProvider } from '@akin-online/partner-sdk';

export default function RootLayout({ children }) {
  return (
    <AkinProvider
      config={{
        partnerId: process.env.NEXT_PUBLIC_AKIN_PARTNER_ID!,
        apiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY!,
        verifyCallbackUrl: process.env.NEXT_PUBLIC_AKIN_VERIFY_URL!, // e.g., https://viajero.com/auth/verify
        environment: 'production',
        debug: false, // Enable for development troubleshooting
      }}
    >
      {children}
    </AkinProvider>
  );
}

2. Use auth hooks

import { useAkinAuth } from '@akin-online/partner-sdk';

function Profile() {
  const { member, isAuthenticated, signOut } = useAkinAuth();

  if (!isAuthenticated) {
    return <p>Please sign in</p>;
  }

  return (
    <div>
      <h1>Welcome, {member?.firstName}!</h1>
      <p>Loyalty #: {member?.loyaltyNumber}</p>
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
}

3. Use headless components for custom UI

import { LoginForm, TierCard, PointsDisplay } from '@akin-online/partner-sdk';

function LoginPage() {
  return (
    <LoginForm onSuccess={() => router.push('/rewards')}>
      {({
        mode,
        setMode,
        email,
        setEmail,
        isLoading,
        error,
        passkeySupported,
        handlePasskeyLogin,
        handleMagicLink,
        clearError,
      }) => (
        <div className="your-styles">
          {/* Passkey login (recommended default) */}
          {mode === 'passkey' && (
            <>
              <button onClick={handlePasskeyLogin} disabled={!passkeySupported || isLoading}>
                Sign in with Passkey
              </button>
              <button onClick={() => setMode('magic-link')}>
                Sign in with Email
              </button>
            </>
          )}

          {/* Magic link (email) login */}
          {mode === 'magic-link' && (
            <>
              <input
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="Email"
              />
              <button onClick={handleMagicLink} disabled={isLoading || !email}>
                Send sign-in link
              </button>
            </>
          )}

          {/* Magic link sent confirmation */}
          {mode === 'magic-link-sent' && (
            <p>Check your email for a sign-in link!</p>
          )}

          {error && <p className="error">{error}</p>}
        </div>
      )}
    </LoginForm>
  );
}

Configuration

interface AkinSDKConfig {
  // Required
  partnerId: string;   // Your partner ID (provided by Akin)
  apiKey: string;      // Your API key (provided by Akin)

  // Recommended
  verifyCallbackUrl?: string; // URL of your /auth/verify page (e.g., https://viajero.com/auth/verify)
                              // Magic link emails will redirect users to this URL

  // Optional - defaults to NEXT_PUBLIC_AKIN_SDK_* env vars
  apiUrl?: string;     // API endpoint (default: NEXT_PUBLIC_AKIN_SDK_API_URL)
  environment?: 'production' | 'staging' | 'development';
  debug?: boolean;     // Enable console logging (default: false)
  firebase?: {         // Firebase/GIP config (default: NEXT_PUBLIC_AKIN_SDK_FIREBASE_* env vars)
    apiKey: string;
    authDomain: string;
    projectId: string;
    appId: string;
  };
  gipTenantId?: string; // GIP tenant ID (default: NEXT_PUBLIC_AKIN_SDK_GIP_TENANT_ID)
}

Authentication Methods

The SDK supports three passwordless authentication methods:

1. Passkeys (WebAuthn) - Recommended

Biometric/device authentication. Most secure and user-friendly.

const { signInWithPasskey } = useAkinAuth();

// Check if passkeys are supported
if (passkeySupported) {
  await signInWithPasskey();
}

2. Magic Links (Email)

Passwordless email verification links.

const { requestMagicLink, verifyMagicLink } = useAkinAuth();

// Request a magic link
await requestMagicLink(email);

// Verify the token (on your /auth/verify page)
const result = await verifyMagicLink(token);
if (result.success) {
  // User is authenticated
}

3. Google OAuth

Social login with Google.

const { signInWithGoogle } = useAkinAuth();

await signInWithGoogle();

Auth API

useAkinAuth Hook

const {
  // State
  user,              // Firebase user object
  member,            // Akin member data (points, tier, etc.)
  isAuthenticated,   // Boolean
  isLoading,         // Boolean
  error,             // Error message or null

  // Actions
  signIn,            // (email, password) => Promise<void>
  signUp,            // (data: SignUpData) => Promise<void>
  signUpPasswordless, // (data) => Promise<{ success, error? }>
  requestMagicLink,  // (email) => Promise<{ success, error? }>
  verifyMagicLink,   // (token) => Promise<{ success, error? }>
  signInWithGoogle,  // () => Promise<void>
  signInWithPasskey, // () => Promise<void>
  signOut,           // () => Promise<void>
  resetPassword,     // (email) => Promise<void>
  refreshMemberData, // () => Promise<void>
  clearError,        // () => void
} = useAkinAuth();

Headless Auth Components

LoginForm

Multi-mode login form supporting passkeys, magic links, and passwords.

<LoginForm onSuccess={onSuccess} onError={onError}>
  {({
    // Mode management
    mode,              // 'passkey' | 'magic-link' | 'magic-link-sent' | 'password'
    setMode,           // (mode) => void

    // Form state
    email, setEmail,
    password, setPassword,
    isLoading, error,

    // Passkey
    passkeySupported,  // Boolean - check before showing passkey option
    handlePasskeyLogin,

    // Magic link
    handleMagicLink,
    handleResendMagicLink,

    // Password (if enabled)
    handleSubmit,

    // Google OAuth
    handleGoogleLogin,

    clearError,
  }) => (
    // Your JSX
  )}
</LoginForm>

SignupForm

Registration form with optional passwordless mode.

<SignupForm
  onSuccess={onSuccess}
  onError={onError}
  passwordless={true}  // Set to true for magic link signup
>
  {({
    firstName, setFirstName,
    lastName, setLastName,
    email, setEmail,
    password, setPassword,      // Only used if passwordless={false}
    phone, setPhone,
    termsAccepted, setTermsAccepted,
    marketingOptIn, setMarketingOptIn,
    isLoading, error,
    handleSubmit,
    handleGoogleSignup,
    clearError,
  }) => (
    // Your JSX
  )}
</SignupForm>

MagicLinkForm

Standalone magic link request form.

<MagicLinkForm onSuccess={onSuccess}>
  {({ email, setEmail, isLoading, error, success, handleSubmit }) => (
    // Your JSX
  )}
</MagicLinkForm>

PhoneInput

International phone number input with country selector.

import { PhoneInput } from '@akin-online/partner-sdk';
import 'react-phone-number-input/style.css';

<PhoneInput
  id="phone"
  value={phone}
  onChange={setPhone}
  defaultCountry="US"
  placeholder="Enter phone number"
  className="your-input-class"
  required
/>

Loyalty API

useAkinLoyalty Hook

const {
  // State
  points,           // Current points balance
  tier,             // 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4'
  tierConfig,       // Current tier configuration
  tierProgress,     // Progress to next tier
  transactions,     // Recent transactions
  tierHistory,      // Tier change history
  isLoading,        // Boolean
  error,            // Error message or null

  // Actions
  refreshData,      // () => Promise<void>
  getTransactionHistory, // (options?) => Promise<Transaction[]>
  getTierHistory,   // (limit?) => Promise<TierHistory[]>
} = useAkinLoyalty();

Headless Loyalty Components

TierCard

<TierCard>
  {({ tier, displayName, color, icon, points, loyaltyNumber, maxPerks }) => (
    // Your JSX
  )}
</TierCard>

TierProgress

<TierProgress>
  {({
    currentTier, currentTierName,
    nextTier, nextTierName,
    percentage, pointsNeeded,
    totalStays, originStays,  // Note: originStays (not originPartnerStays)
  }) => (
    nextTierName ? (
      <div>
        <progress value={percentage} max={100} />
        <p>{pointsNeeded} points to {nextTierName}</p>
      </div>
    ) : (
      <p>You've reached the highest tier!</p>
    )
  )}
</TierProgress>

TransactionList

<TransactionList limit={10}>
  {({ transactions, isLoading, hasMore, loadMore }) => (
    // Your JSX
  )}
</TransactionList>

PointsDisplay

<PointsDisplay>
  {({ points, formattedPoints, isLoading }) => (
    // Your JSX
  )}
</PointsDisplay>

TierRequirementsTable

Display tier requirements in a table format. Perfect for "how to qualify" sections.

import { TierRequirementsTable } from '@akin-online/partner-sdk';

<TierRequirementsTable partnerName="Haka">
  {({ tierConfigs, requirementKeys, getRequirementLabel, formatValue, getRequirement, isLoading }) => (
    <table>
      <thead>
        <tr>
          <th>Requirement</th>
          {tierConfigs.map(tier => (
            <th key={tier.id}>{tier.displayName}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {requirementKeys.map(key => (
          <tr key={key}>
            <td>{getRequirementLabel(key)}</td>
            {tierConfigs.map(tier => {
              const req = getRequirement(tier, key);
              return (
                <td key={tier.id}>
                  {req ? formatValue(Number(req.requirementValue), req.operator) : '—'}
                </td>
              );
            })}
          </tr>
        ))}
      </tbody>
    </table>
  )}
</TierRequirementsTable>

Render Props:

  • tierConfigs - Tier configs sorted by display order
  • requirementKeys - Unique requirement keys sorted by priority
  • getRequirementLabel(key, displayName?) - Get display label for a key
  • formatValue(value, operator) - Format value with operator (e.g., "10+")
  • getRequirement(tier, key) - Get requirement for tier/key
  • partnerName - Partner name used for dynamic labels
  • isLoading - Loading state

SimpleTierCards

Display tier cards with requirements. Great for tier overview sections.

import { SimpleTierCards } from '@akin-online/partner-sdk';

<SimpleTierCards partnerName="Viajero">
  {({ tierConfigs, formatRequirement, getTextColor, isLoading }) => (
    <div>
      <h2 className="text-2xl font-bold text-center mb-6">Viajero GO Tiers</h2>
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-stretch">
        {tierConfigs.map((tier, index) => {
          const bgColor = tier.tierColor || '#382108';
          const textColor = getTextColor(bgColor);
          return (
            <div
              key={tier.id}
              style={{ backgroundColor: bgColor, color: textColor }}
              className="p-5 rounded-xl flex flex-col h-full"
            >
              {/* Tier icon */}
              {tier.tierIcon && (
                <div className="w-10 h-10 mb-3 rounded-full bg-white/20 flex items-center justify-center">
                  <img src={tier.tierIcon} alt="" className="w-6 h-6" />
                </div>
              )}
              {/* Tier name */}
              <h3 className="text-2xl font-bold">{tier.displayName}</h3>
              <span className="text-sm opacity-70 mb-3">Tier {index + 1}</span>
              {/* Requirements */}
              <div className="mt-auto pt-3 border-t border-current/20 space-y-1">
                {tier.requirements?.filter(r => r.active).map(req => (
                  <p key={req.id} className="text-xs opacity-80">
                    {formatRequirement(req)}
                  </p>
                ))}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  )}
</SimpleTierCards>

Render Props:

  • tierConfigs - Tier configs sorted by display order
  • formatRequirement(req) - Format requirement (e.g., "10+ Origin stays")
  • getTextColor(bgColor) - Get contrasting text color for background
  • isLoading - Loading state

TierBenefits

Display rewards/benefits available at each tier. Fetched automatically via the loyalty provider.

import { TierBenefits } from '@akin-online/partner-sdk';

<TierBenefits>
  {({ tierBenefits, getTierConfig, getTextColor, isLoading }) => (
    <div>
      <h2 className="text-2xl font-bold text-center mb-6">Tier Benefits</h2>
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-stretch">
        {tierBenefits.map((benefit) => {
          const config = getTierConfig(benefit.tierName);
          const bgColor = config?.tierColor || '#382108';
          const textColor = config?.tierTextColor || getTextColor(bgColor);
          return (
            <div
              key={benefit.tierName}
              style={{ backgroundColor: bgColor, color: textColor }}
              className="p-5 rounded-xl flex flex-col h-full"
            >
              <h3 className="text-xl font-bold mb-2">{config?.displayName}</h3>
              <div className="w-full h-px mb-3 opacity-30 bg-current" />
              {benefit.groups?.some(g => g.scope === 'PROPERTY') ? (
                /* Grouped by property */
                <div className="space-y-3 flex-1">
                  {benefit.groups.map((group) => (
                    <div key={group.scope === 'ALL_PROPERTIES' ? 'all' : group.propertyId}>
                      <h4 className="text-xs font-semibold uppercase tracking-wide mb-1 opacity-70">
                        {group.scope === 'ALL_PROPERTIES' ? 'All Properties' : group.propertyName}
                      </h4>
                      <ul className="space-y-1">
                        {group.rewardNames.map((name, i) => (
                          <li key={i} className="text-sm flex items-start gap-2">
                            <span className="mt-0.5">✓</span>
                            <span>{name}</span>
                          </li>
                        ))}
                      </ul>
                    </div>
                  ))}
                </div>
              ) : (
                /* Flat list */
                <ul className="space-y-1.5 flex-1">
                  {benefit.rewardNames.map((name, i) => (
                    <li key={i} className="text-sm flex items-start gap-2">
                      <span className="mt-0.5">✓</span>
                      <span>{name}</span>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          );
        })}
      </div>
    </div>
  )}
</TierBenefits>

Render Props:

  • tierBenefits - Tier benefits sorted by display order, each with tierName, rewardNames, and optional groups
  • tierConfigs - Tier configs sorted by display order (for colors/names)
  • getTierConfig(tierName) - Get tier config for a given tier name
  • getTextColor(bgColor) - Get contrasting text color for background
  • isLoading - Loading state

Tier Label Utilities

Centralized labels for tier requirement keys.

import { getLabel, TIER_LABELS, PARTNER_LABELS, KEY_PRIORITY, extractRequirementKeys } from '@akin-online/partner-sdk';

// Get a label for a requirement key
getLabel('stays_member_origin');           // "Origin stays"
getLabel('stays_partner', 'Haka');         // "Haka stays"
getLabel('points', 'Origin', 'Custom');    // "Custom" (displayName override)

// Static labels
TIER_LABELS['points'];                     // "AKIN points"
TIER_LABELS['stays_network'];              // "Network stays"

// Dynamic labels (with partner name)
PARTNER_LABELS['stays_partner']('Haka');   // "Haka stays"
PARTNER_LABELS['achievement_tattoo']('Drifter'); // "Drifter tattoo"

// Extract unique requirement keys from tier configs (sorted by priority)
const keys = extractRequirementKeys(tierConfigs);
// ['stays_partner', 'stays_member_origin', 'points', ...]

Available Labels:

  • stays_member_origin → "Origin stays"
  • stays_network → "Network stays"
  • stays_partner → "[Partner] stays"
  • nights_member_origin → "Origin nights"
  • nights_network → "Network nights"
  • nights_partner → "[Partner] nights"
  • points → "AKIN points"
  • network_points → "Network points"
  • spend → "Total spend"
  • referrals → "Referrals"
  • achievement_tattoo → "[Partner] tattoo"
  • achievement_invitation → "Invitation"
  • country_completion_mx → "Mexico complete"
  • country_completion_pe → "Peru complete"
  • country_completion_co → "Colombia complete"

Types

AkinMember

interface AkinMember {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  phone?: string;
  points: number;
  currentTier: 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4';
  status: 'ACTIVE' | 'SUSPENDED' | 'DELETED';
  loyaltyNumber: string;
  vibePreference?: string;
  perkPreferences?: string[];
  createdAt: Date;
  updatedAt: Date;
}

TierConfig

interface TierConfig {
  id: string;
  tierName: string;        // 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4'
  displayName: string;     // 'Explorer' | 'Voyager' | etc.
  displayOrder: number;
  maxPerksAllowed: number;
  tierColor?: string;
  tierIcon?: string;
  tierByline?: string;
  eligibility?: string;    // Note: eligibility (not eligibilityText)
  requirements?: TierRequirement[];  // Key/value tier requirements
}

TierRequirement

interface TierRequirement {
  id: string;
  tierConfigId: string;
  requirementKey: string;      // e.g., 'stays_member_origin', 'points'
  requirementValue: string;    // Threshold value
  valueType: 'INT' | 'FLOAT' | 'DATE' | 'STRING';
  operator: 'eq' | 'gt' | 'gte' | 'lt' | 'lte';
  logicGroup: number;          // For AND/OR logic combinations
  displayOrder: number;
  displayName?: string;        // Custom display override
  active: boolean;
}

LoyaltyTransaction

interface LoyaltyTransaction {
  id: string;
  memberId: string;
  bookingId?: string;
  pointsChange: number;
  transactionType: 'EARNED' | 'SPENT' | 'EXPIRED' | 'ADJUSTED';
  reason?: string;
  description?: string;
  balanceAfter: number;
  createdAt: Date;
}

TierProgress

interface TierProgress {
  currentTier: string;
  currentTierName: string;
  nextTier: string | null;
  nextTierName: string | null;
  percentage: number;
  pointsNeeded: number;
  totalStays: number;
  originStays: number;  // Note: originStays (not originPartnerStays)
}

Environment Variables

# Required - Partner credentials
NEXT_PUBLIC_AKIN_PARTNER_ID=your-partner-id
NEXT_PUBLIC_AKIN_API_KEY=your-api-key
NEXT_PUBLIC_AKIN_VERIFY_URL=https://yourdomain.com/auth/verify

# Required - SDK infrastructure (provided by Akin during onboarding)
NEXT_PUBLIC_AKIN_SDK_API_URL=https://api.akintravel.com/graphql
NEXT_PUBLIC_AKIN_SDK_GIP_TENANT_ID=your-gip-tenant-id
NEXT_PUBLIC_AKIN_SDK_FIREBASE_API_KEY=your-firebase-api-key
NEXT_PUBLIC_AKIN_SDK_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_AKIN_SDK_FIREBASE_PROJECT_ID=your-firebase-project-id
NEXT_PUBLIC_AKIN_SDK_FIREBASE_APP_ID=your-firebase-app-id

# Optional - Environment-specific API URLs
NEXT_PUBLIC_AKIN_SDK_STAGING_API_URL=https://staging-api.example.com/graphql
NEXT_PUBLIC_AKIN_SDK_DEV_API_URL=http://localhost:4000/graphql

Security Best Practices

API Key Handling

IMPORTANT: Always use environment variables for your API key. Never commit API keys to source control.

// ✅ CORRECT - Use environment variable
apiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY!

// ❌ WRONG - Never hardcode API keys
apiKey: 'pk_live_abc123...'

// ❌ WRONG - Never use fallback values in production
apiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY || 'demo-key'

Debug Mode

Only enable debug mode in development:

debug: process.env.NODE_ENV === 'development'

Environment File (.env.local)

All SDK configuration values (API URLs, Firebase credentials, GIP tenant IDs) must be set via NEXT_PUBLIC_AKIN_SDK_* environment variables. These values are provided during partner onboarding and should never be hardcoded in source code.

# Add to .gitignore
.env.local
.env*.local

CORS

The SDK automatically handles CORS with the Akin API. If you need to allow additional origins, contact your Akin partner representative.

CSS Styling Notes

Link Color Override

If you have global link styles that override button text colors, use a more specific selector:

/* ❌ This will override button text colors */
a {
  @apply text-blue-600;
}

/* ✅ Use this instead - excludes elements with explicit text colors */
a:not([class*="text-"]) {
  @apply text-blue-600;
}

Phone Input Styles

Import the phone input CSS:

import 'react-phone-number-input/style.css';

Examples

Magic Link Verification Page

Create an /auth/verify route to handle magic link callbacks:

'use client';

import { Suspense, useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useAkinAuth } from '@akin-online/partner-sdk';

function VerifyContent() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const { verifyMagicLink } = useAkinAuth();
  const [state, setState] = useState<'loading' | 'success' | 'error'>('loading');
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const token = searchParams.get('token');

    if (!token) {
      setError('Invalid verification link');
      setState('error');
      return;
    }

    const verify = async () => {
      const result = await verifyMagicLink(token);

      if (!result.success) {
        setError(result.error || 'Verification failed');
        setState('error');
        return;
      }

      setState('success');
      setTimeout(() => router.push('/rewards'), 1500);
    };

    verify();
  }, [searchParams, verifyMagicLink, router]);

  if (state === 'loading') return <p>Verifying...</p>;
  if (state === 'success') return <p>Verified! Redirecting...</p>;
  return <p>Error: {error}</p>;
}

export default function VerifyPage() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <VerifyContent />
    </Suspense>
  );
}

Complete Passwordless Signup

'use client';

import { useState } from 'react';
import { SignupForm, PhoneInput } from '@akin-online/partner-sdk';
import 'react-phone-number-input/style.css';

export default function SignupPage() {
  const [signupComplete, setSignupComplete] = useState(false);
  const [submittedEmail, setSubmittedEmail] = useState('');

  if (signupComplete) {
    return (
      <div className="text-center">
        <h1>Check your email!</h1>
        <p>We sent a verification link to {submittedEmail}</p>
      </div>
    );
  }

  return (
    <SignupForm
      passwordless
      onSuccess={() => {}}
      onError={(error) => console.error(error)}
    >
      {({
        firstName, setFirstName,
        lastName, setLastName,
        email, setEmail,
        phone, setPhone,
        termsAccepted, setTermsAccepted,
        marketingOptIn, setMarketingOptIn,
        isLoading, error,
        handleSubmit,
        clearError,
      }) => (
        <form
          onSubmit={(e) => {
            e.preventDefault();
            setSubmittedEmail(email);
            handleSubmit(e).then(() => {
              if (!error) setSignupComplete(true);
            });
          }}
        >
          <div className="grid grid-cols-2 gap-4">
            <input
              value={firstName}
              onChange={(e) => { setFirstName(e.target.value); clearError(); }}
              placeholder="First name"
              required
            />
            <input
              value={lastName}
              onChange={(e) => { setLastName(e.target.value); clearError(); }}
              placeholder="Last name"
              required
            />
          </div>

          <input
            type="email"
            value={email}
            onChange={(e) => { setEmail(e.target.value); clearError(); }}
            placeholder="Email"
            required
          />

          <PhoneInput
            value={phone}
            onChange={(value) => { setPhone(value); clearError(); }}
            defaultCountry="US"
            placeholder="Phone number"
            required
          />

          <label>
            <input
              type="checkbox"
              checked={termsAccepted}
              onChange={(e) => setTermsAccepted(e.target.checked)}
              required
            />
            I agree to the Terms of Service and Privacy Policy
          </label>

          <label>
            <input
              type="checkbox"
              checked={marketingOptIn}
              onChange={(e) => setMarketingOptIn(e.target.checked)}
            />
            Send me updates and offers
          </label>

          {error && <p className="error">{error}</p>}

          <button type="submit" disabled={isLoading}>
            {isLoading ? 'Creating account...' : 'Create Account'}
          </button>
        </form>
      )}
    </SignupForm>
  );
}

Complete Login Page with Passkeys

'use client';

import { useAkinAuth, LoginForm } from '@akin-online/partner-sdk';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export default function LoginPage() {
  const router = useRouter();
  const { isAuthenticated } = useAkinAuth();

  useEffect(() => {
    if (isAuthenticated) router.push('/rewards');
  }, [isAuthenticated, router]);

  return (
    <div className="login-container">
      <h1>Welcome Back</h1>

      <LoginForm
        onSuccess={() => router.push('/rewards')}
        onError={(error) => console.error(error)}
      >
        {({
          mode, setMode,
          email, setEmail,
          isLoading, error,
          passkeySupported,
          handlePasskeyLogin,
          handleMagicLink,
          handleResendMagicLink,
          clearError,
        }) => (
          <div>
            {error && <p className="error">{error}</p>}

            {mode === 'passkey' && (
              <div>
                <button
                  onClick={handlePasskeyLogin}
                  disabled={!passkeySupported || isLoading}
                >
                  {isLoading ? 'Signing in...' : 'Sign in with Passkey'}
                </button>
                <button onClick={() => { clearError(); setMode('magic-link'); }}>
                  Sign in with Email
                </button>
              </div>
            )}

            {mode === 'magic-link' && (
              <div>
                <input
                  type="email"
                  value={email}
                  onChange={(e) => { setEmail(e.target.value); clearError(); }}
                  placeholder="[email protected]"
                />
                <button
                  onClick={handleMagicLink}
                  disabled={isLoading || !email}
                >
                  {isLoading ? 'Sending...' : 'Send sign-in link'}
                </button>
                {passkeySupported && (
                  <button onClick={() => { clearError(); setMode('passkey'); }}>
                    ← Back to passkey sign-in
                  </button>
                )}
              </div>
            )}

            {mode === 'magic-link-sent' && (
              <div className="text-center">
                <h2>Check your email</h2>
                <p>We sent a sign-in link to {email}</p>
                <button onClick={handleResendMagicLink} disabled={isLoading}>
                  {isLoading ? 'Resending...' : 'Resend link'}
                </button>
                <button onClick={() => { clearError(); setMode('magic-link'); }}>
                  Try a different email
                </button>
              </div>
            )}
          </div>
        )}
      </LoginForm>
    </div>
  );
}

Complete Rewards Dashboard

'use client';

import {
  useAkinAuth,
  useAkinLoyalty,
  TierCard,
  TierProgress,
  TransactionList,
  PointsDisplay,
} from '@akin-online/partner-sdk';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export default function RewardsPage() {
  const router = useRouter();
  const { member, isAuthenticated, isLoading: authLoading, signOut } = useAkinAuth();
  const { isLoading: loyaltyLoading } = useAkinLoyalty();

  useEffect(() => {
    if (!authLoading && !isAuthenticated) {
      router.push('/login');
    }
  }, [isAuthenticated, authLoading, router]);

  if (authLoading || loyaltyLoading) {
    return <p>Loading...</p>;
  }

  if (!isAuthenticated) return null;

  return (
    <div className="rewards-container">
      <header>
        <h1>Welcome back, {member?.firstName}!</h1>
        <p>Loyalty #: {member?.loyaltyNumber}</p>
        <button onClick={() => signOut().then(() => router.push('/'))}>
          Sign Out
        </button>
      </header>

      <div className="grid grid-cols-2 gap-4">
        <div className="card">
          <h2>Your Points</h2>
          <PointsDisplay>
            {({ formattedPoints }) => (
              <p className="text-4xl font-bold">{formattedPoints}</p>
            )}
          </PointsDisplay>
        </div>

        <div className="card">
          <h2>Current Tier</h2>
          <TierCard>
            {({ displayName, tier }) => (
              <div>
                <p className="text-2xl font-bold">{displayName}</p>
                <p className="text-sm">Tier {tier.replace('TIER', '')}</p>
              </div>
            )}
          </TierCard>
        </div>
      </div>

      <div className="card">
        <h2>Progress to Next Tier</h2>
        <TierProgress>
          {({ currentTierName, nextTierName, percentage, pointsNeeded }) => (
            nextTierName ? (
              <div>
                <div className="flex justify-between">
                  <span>{currentTierName}</span>
                  <span>{nextTierName}</span>
                </div>
                <div className="progress-bar">
                  <div style={{ width: `${percentage}%` }} />
                </div>
                <p>{pointsNeeded.toLocaleString()} points to {nextTierName}</p>
              </div>
            ) : (
              <p>You've reached the highest tier!</p>
            )
          )}
        </TierProgress>
      </div>

      <div className="card">
        <h2>Recent Activity</h2>
        <TransactionList limit={10}>
          {({ transactions, isLoading, hasMore, loadMore }) => (
            <div>
              {transactions.length === 0 ? (
                <p>No transactions yet</p>
              ) : (
                <>
                  <ul>
                    {transactions.map((tx) => (
                      <li key={tx.id} className="flex justify-between py-3">
                        <div>
                          <p>{tx.reason || tx.description || 'Transaction'}</p>
                          <p className="text-sm text-gray-500">
                            {new Date(tx.createdAt).toLocaleDateString()}
                          </p>
                        </div>
                        <span className={tx.pointsChange > 0 ? 'text-green-600' : 'text-red-600'}>
                          {tx.pointsChange > 0 ? '+' : ''}{tx.pointsChange.toLocaleString()}
                        </span>
                      </li>
                    ))}
                  </ul>
                  {hasMore && (
                    <button onClick={loadMore} disabled={isLoading}>
                      {isLoading ? 'Loading...' : 'Load More'}
                    </button>
                  )}
                </>
              )}
            </div>
          )}
        </TransactionList>
      </div>
    </div>
  );
}

Development / Examples

Viajero Demo

A complete example implementation is available in the monorepo at examples/viajero/. This demonstrates a partner rewards site using the SDK with:

  • Login page with passkey and magic link authentication
  • Signup form with passwordless registration
  • Rewards dashboard with loyalty card, tier cards, tier benefits, upcoming stays, and activity statement
  • Full Tailwind CSS styling with shadcn/ui components

Running the demo:

# From monorepo root
cd examples/viajero
npm install
npm run dev

The demo runs at http://localhost:3003

Demo structure:

viajero/
├── app/
│   ├── layout.tsx      # AkinProvider setup
│   ├── page.tsx        # Landing page
│   ├── login/          # Login with passkey/magic link
│   ├── signup/         # Passwordless signup
│   ├── account/        # Authenticated rewards dashboard
│   ├── settings/       # Account settings
│   └── auth/verify/    # Magic link verification
├── components/         # shadcn/ui components
└── .env.local.example  # Environment variables template

Local SDK Development

When developing the SDK itself, the demo uses a file reference to the local package:

{
  "dependencies": {
    "@akin-travel/partner-sdk": "file:../../packages/partner-sdk"
  }
}

After making changes to the SDK, rebuild and restart the demo:

# From monorepo root
npm run build -w @akin-online/partner-sdk
cd examples/viajero && npm run dev

Troubleshooting

"Loading..." stuck after passkey authentication

Ensure you're using the latest SDK version. The signInWithPasskey function properly resets loading state in the finally block.

Button text color not showing

Check for global CSS rules that override text colors. See CSS Styling Notes.

Phone input not styled

Import the phone input CSS: import 'react-phone-number-input/style.css';

Magic link not working

  1. Verify your NEXT_PUBLIC_AKIN_API_KEY is set correctly
  2. Check that your domain is in the allowed origins for your API key
  3. Ensure you have an /auth/verify route to handle the callback

GraphQL errors

If you see errors about field types or non-existent fields, ensure you're using the latest SDK version. Common issues:

  • Use String! (not ID!) for member/partner IDs in queries
  • Use originStays (not originPartnerStays)
  • Use eligibility (not eligibilityText)

Support

For questions or issues, contact your Akin partner representative or email [email protected]