{"_id":"@akin-travel/partner-sdk","_rev":"15-fe39a08b14f0cc04a924cd3897bac4be","name":"@akin-travel/partner-sdk","dist-tags":{"latest":"1.0.8"},"versions":{"1.0.5":{"name":"@akin-travel/partner-sdk","version":"1.0.5","keywords":["akin","loyalty","auth","sdk","react","headless"],"license":"UNLICENSED","_id":"@akin-travel/partner-sdk@1.0.5","maintainers":[{"name":"seangee","email":"seangomer@akintravel.com"}],"homepage":"https://github.com/akin-travel/akin-online#readme","bugs":{"url":"https://github.com/akin-travel/akin-online/issues"},"dist":{"shasum":"fc4631b3a67a2b53666fb7b5e76d02a30ca14b77","tarball":"https://registry.npmjs.org/@akin-travel/partner-sdk/-/partner-sdk-1.0.5.tgz","fileCount":204,"integrity":"sha512-31Db5OWRq8w8p+dmKoz+/Xov8whXxQ6TO4IvmzSiyiD0+b33U5exAlmtIov8yPc7fHZ0leZ46TinYEfe6OEhIw==","signatures":[{"sig":"MEQCIEuVfSxmRPikF8ulGEsY3j9r/fMZX5eXSpXa5c7i3BRLAiBQ51PZ6PaVyGCWGhbU8WQCu81aG8y8OxtBWtiKKoZ7bw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":466956},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"188a8e744393148546453cdf62b723fa32b16959","private":false,"scripts":{"dev":"tsc --watch","lint":"eslint src/**/*.ts src/**/*.tsx","test":"vitest run --passWithNoTests","build":"tsc","clean":"rm -rf dist tsconfig.tsbuildinfo","format":"prettier --write src/**/*.ts src/**/*.tsx","prebuild":"npm run clean","prepublishOnly":"npm run build"},"_npmUser":{"name":"seangee","email":"seangomer@akintravel.com"},"repository":{"url":"git+https://github.com/akin-travel/akin-online.git","type":"git","directory":"packages/partner-sdk"},"_npmVersion":"10.9.4","description":"SDK for third-party partners to integrate Akin auth and loyalty services","directories":{},"_nodeVersion":"22.21.0","dependencies":{"graphql":"^16.8.1","firebase":"^11.7.0","@apollo/client":"^3.8.8","@simplewebauthn/browser":"^13.2.2","react-phone-number-input":"^3.4.9"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.0.0","eslint":"^9.33.0","vitest":"^4.0.6","prettier":"^3.1.0","react-dom":"^19.0.0","typescript":"~5.7.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"_npmOperationalInternal":{"tmp":"tmp/partner-sdk_1.0.5_1770768546715_0.6564483992121195","host":"s3://npm-registry-packages-npm-production"}},"1.0.6":{"name":"@akin-travel/partner-sdk","version":"1.0.6","keywords":["akin","loyalty","auth","sdk","react","headless"],"license":"UNLICENSED","_id":"@akin-travel/partner-sdk@1.0.6","maintainers":[{"name":"seangee","email":"seangomer@akintravel.com"}],"homepage":"https://github.com/akin-travel/akin-online#readme","bugs":{"url":"https://github.com/akin-travel/akin-online/issues"},"dist":{"shasum":"4dd44b27005035bf1823b215be7519faa1c183fd","tarball":"https://registry.npmjs.org/@akin-travel/partner-sdk/-/partner-sdk-1.0.6.tgz","fileCount":208,"integrity":"sha512-cdzQtCkIfUeSrlmHvsqRRa9eDzDupMr/qZWMPNkLgIEms3bvVIqWHe0705/IcknAnhTJLXl+b4pmzbyoEIpqTg==","signatures":[{"sig":"MEUCIQDZKW91qXFDKhaFisokJ/Oprwms/rFkhvjX4LVPj9X+GgIgJ0lnV0JK5+1sMxgBueZRp+StwTDN3aWv6zmAAw6Xdiw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":487978},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"a644b3018713fc7d8fb8326a36f9353eec90615d","private":false,"scripts":{"dev":"tsc --watch","lint":"eslint src/**/*.ts src/**/*.tsx","test":"vitest run --passWithNoTests","build":"tsc","clean":"rm -rf dist tsconfig.tsbuildinfo","format":"prettier --write src/**/*.ts src/**/*.tsx","prebuild":"npm run clean","prepublishOnly":"npm run build"},"_npmUser":{"name":"seangee","email":"seangomer@akintravel.com"},"repository":{"url":"git+https://github.com/akin-travel/akin-online.git","type":"git","directory":"packages/partner-sdk"},"_npmVersion":"10.9.4","description":"SDK for third-party partners to integrate Akin auth and loyalty services","directories":{},"_nodeVersion":"22.21.0","dependencies":{"graphql":"^16.8.1","firebase":"^11.7.0","@apollo/client":"^3.8.8","@simplewebauthn/browser":"^13.2.2","react-phone-number-input":"^3.4.9"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.0.0","eslint":"^9.33.0","vitest":"^4.0.6","prettier":"^3.1.0","react-dom":"^19.0.0","typescript":"~5.7.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"_npmOperationalInternal":{"tmp":"tmp/partner-sdk_1.0.6_1771390506778_0.1932225772659033","host":"s3://npm-registry-packages-npm-production"}},"1.0.8":{"name":"@akin-travel/partner-sdk","version":"1.0.8","description":"SDK for third-party partners to integrate Akin auth and loyalty services","main":"dist/index.js","types":"dist/index.d.ts","publishConfig":{"access":"public"},"scripts":{"prebuild":"npm run clean","build":"tsc","dev":"tsc --watch","clean":"rm -rf dist tsconfig.tsbuildinfo","test":"vitest run --passWithNoTests","lint":"eslint src/**/*.ts src/**/*.tsx","format":"prettier --write src/**/*.ts src/**/*.tsx","prepublishOnly":"npm run build"},"dependencies":{"@apollo/client":"^3.8.8","@simplewebauthn/browser":"^13.2.2","firebase":"^11.7.0","graphql":"^16.8.1","react-phone-number-input":"^3.4.9"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"devDependencies":{"@types/react":"^19.0.0","@types/react-dom":"^19.0.0","eslint":"^9.33.0","prettier":"^3.1.0","react":"^19.0.0","react-dom":"^19.0.0","typescript":"~5.7.2","vitest":"^4.0.6"},"keywords":["akin","loyalty","auth","sdk","react","headless"],"repository":{"type":"git","url":"git+https://github.com/akin-travel/akin-online.git","directory":"packages/partner-sdk"},"license":"UNLICENSED","private":false,"_id":"@akin-travel/partner-sdk@1.0.8","gitHead":"71e65ea5694390b9f1a668eac4c46318f7797c05","bugs":{"url":"https://github.com/akin-travel/akin-online/issues"},"homepage":"https://github.com/akin-travel/akin-online#readme","_nodeVersion":"22.21.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-kUgQExv/eiSxnonI5D0JfFwPX8242cdptXeSA7jLlGj7zZQ8tqj2yS4gVKvlsNNIYnBInQt0IQESFA/aplcs3Q==","shasum":"0ef5854e87db99529179a9c701c9bb0aa280ccc0","tarball":"https://registry.npmjs.org/@akin-travel/partner-sdk/-/partner-sdk-1.0.8.tgz","fileCount":208,"unpackedSize":489353,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD8OxXA5F6XDbqpHFy5eBSAFS+zBIlDLqR8yQ0E/tHQ1QIgKQz60R+MnZ2feJ62esIyeuNbRm5EjlAZb0U6gd+hlTo="}]},"_npmUser":{"name":"seangee","email":"seangomer@akintravel.com"},"directories":{},"maintainers":[{"name":"seangee","email":"seangomer@akintravel.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/partner-sdk_1.0.8_1771546546003_0.5312929898651493"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-22T01:13:19.274Z","modified":"2026-02-20T00:15:46.321Z","1.0.0":"2026-01-22T01:13:19.582Z","1.0.1":"2026-01-22T01:14:37.846Z","1.0.2":"2026-01-29T23:08:57.575Z","1.0.3":"2026-02-03T02:55:23.067Z","1.0.4":"2026-02-06T07:03:45.795Z","1.0.5":"2026-02-11T00:09:06.914Z","1.0.6":"2026-02-18T04:55:06.956Z","1.0.8":"2026-02-20T00:15:46.177Z"},"bugs":{"url":"https://github.com/akin-travel/akin-online/issues"},"license":"UNLICENSED","homepage":"https://github.com/akin-travel/akin-online#readme","keywords":["akin","loyalty","auth","sdk","react","headless"],"repository":{"type":"git","url":"git+https://github.com/akin-travel/akin-online.git","directory":"packages/partner-sdk"},"description":"SDK for third-party partners to integrate Akin auth and loyalty services","maintainers":[{"name":"seangee","email":"seangomer@akintravel.com"}],"readme":"# @akin-online/partner-sdk\n\nSDK for third-party partners to integrate Akin auth and loyalty services into their React applications.\n\n## Features\n\n- **Authentication** - Magic links (passwordless), passkeys (WebAuthn), Google OAuth\n- **Loyalty** - Tier display, points balance, transaction history, progress tracking\n- **Tier Requirements** - Flexible key/value qualification rules with display utilities\n- **Headless Components** - Full control over your UI with render props\n- **TypeScript** - Full type support\n- **Phone Input** - International phone number input with validation\n\n## Getting Access\n\nThe `@akin-online/partner-sdk` is a **private npm package** available to authorized Akin partners.\n\n### Partner Onboarding\n\nContact your Akin partner representative or email partners@akintravel.com to request access. You'll receive the following via secure channel:\n\n| Credential | Purpose |\n|------------|---------|\n| **npm access token** | For installing the private `@akin-online/partner-sdk` package |\n| **Partner ID** | Your unique identifier in the Akin system |\n| **API key** | For authenticating with the Akin API |\n| **SDK environment variables** | Firebase/GIP credentials and API URLs (`NEXT_PUBLIC_AKIN_SDK_*`) |\n\n### Configure npm Access\n\nAdd the npm access token to your `.npmrc` file:\n\n```bash\n# ~/.npmrc or project .npmrc\n//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN\n```\n\n**Security Note:** Never commit your `.npmrc` file with tokens to source control. Add it to `.gitignore`.\n\n## Installation\n\n```bash\nnpm install @akin-online/partner-sdk\n```\n\n### Peer Dependencies\n\n```bash\nnpm install react-phone-number-input\n```\n\n## Quick Start\n\n### 1. Wrap your app with AkinProvider\n\n```tsx\n// app/layout.tsx or _app.tsx\nimport { AkinProvider } from '@akin-online/partner-sdk';\n\nexport default function RootLayout({ children }) {\n  return (\n    <AkinProvider\n      config={{\n        partnerId: process.env.NEXT_PUBLIC_AKIN_PARTNER_ID!,\n        apiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY!,\n        verifyCallbackUrl: process.env.NEXT_PUBLIC_AKIN_VERIFY_URL!, // e.g., https://viajero.com/auth/verify\n        environment: 'production',\n        debug: false, // Enable for development troubleshooting\n      }}\n    >\n      {children}\n    </AkinProvider>\n  );\n}\n```\n\n### 2. Use auth hooks\n\n```tsx\nimport { useAkinAuth } from '@akin-online/partner-sdk';\n\nfunction Profile() {\n  const { member, isAuthenticated, signOut } = useAkinAuth();\n\n  if (!isAuthenticated) {\n    return <p>Please sign in</p>;\n  }\n\n  return (\n    <div>\n      <h1>Welcome, {member?.firstName}!</h1>\n      <p>Loyalty #: {member?.loyaltyNumber}</p>\n      <button onClick={signOut}>Sign Out</button>\n    </div>\n  );\n}\n```\n\n### 3. Use headless components for custom UI\n\n```tsx\nimport { LoginForm, TierCard, PointsDisplay } from '@akin-online/partner-sdk';\n\nfunction LoginPage() {\n  return (\n    <LoginForm onSuccess={() => router.push('/rewards')}>\n      {({\n        mode,\n        setMode,\n        email,\n        setEmail,\n        isLoading,\n        error,\n        passkeySupported,\n        handlePasskeyLogin,\n        handleMagicLink,\n        clearError,\n      }) => (\n        <div className=\"your-styles\">\n          {/* Passkey login (recommended default) */}\n          {mode === 'passkey' && (\n            <>\n              <button onClick={handlePasskeyLogin} disabled={!passkeySupported || isLoading}>\n                Sign in with Passkey\n              </button>\n              <button onClick={() => setMode('magic-link')}>\n                Sign in with Email\n              </button>\n            </>\n          )}\n\n          {/* Magic link (email) login */}\n          {mode === 'magic-link' && (\n            <>\n              <input\n                type=\"email\"\n                value={email}\n                onChange={(e) => setEmail(e.target.value)}\n                placeholder=\"Email\"\n              />\n              <button onClick={handleMagicLink} disabled={isLoading || !email}>\n                Send sign-in link\n              </button>\n            </>\n          )}\n\n          {/* Magic link sent confirmation */}\n          {mode === 'magic-link-sent' && (\n            <p>Check your email for a sign-in link!</p>\n          )}\n\n          {error && <p className=\"error\">{error}</p>}\n        </div>\n      )}\n    </LoginForm>\n  );\n}\n```\n\n## Configuration\n\n```typescript\ninterface AkinSDKConfig {\n  // Required\n  partnerId: string;   // Your partner ID (provided by Akin)\n  apiKey: string;      // Your API key (provided by Akin)\n\n  // Recommended\n  verifyCallbackUrl?: string; // URL of your /auth/verify page (e.g., https://viajero.com/auth/verify)\n                              // Magic link emails will redirect users to this URL\n\n  // Optional - defaults to NEXT_PUBLIC_AKIN_SDK_* env vars\n  apiUrl?: string;     // API endpoint (default: NEXT_PUBLIC_AKIN_SDK_API_URL)\n  environment?: 'production' | 'staging' | 'development';\n  debug?: boolean;     // Enable console logging (default: false)\n  firebase?: {         // Firebase/GIP config (default: NEXT_PUBLIC_AKIN_SDK_FIREBASE_* env vars)\n    apiKey: string;\n    authDomain: string;\n    projectId: string;\n    appId: string;\n  };\n  gipTenantId?: string; // GIP tenant ID (default: NEXT_PUBLIC_AKIN_SDK_GIP_TENANT_ID)\n}\n```\n\n## Authentication Methods\n\nThe SDK supports three passwordless authentication methods:\n\n### 1. Passkeys (WebAuthn) - Recommended\n\nBiometric/device authentication. Most secure and user-friendly.\n\n```tsx\nconst { signInWithPasskey } = useAkinAuth();\n\n// Check if passkeys are supported\nif (passkeySupported) {\n  await signInWithPasskey();\n}\n```\n\n### 2. Magic Links (Email)\n\nPasswordless email verification links.\n\n```tsx\nconst { requestMagicLink, verifyMagicLink } = useAkinAuth();\n\n// Request a magic link\nawait requestMagicLink(email);\n\n// Verify the token (on your /auth/verify page)\nconst result = await verifyMagicLink(token);\nif (result.success) {\n  // User is authenticated\n}\n```\n\n### 3. Google OAuth\n\nSocial login with Google.\n\n```tsx\nconst { signInWithGoogle } = useAkinAuth();\n\nawait signInWithGoogle();\n```\n\n## Auth API\n\n### useAkinAuth Hook\n\n```typescript\nconst {\n  // State\n  user,              // Firebase user object\n  member,            // Akin member data (points, tier, etc.)\n  isAuthenticated,   // Boolean\n  isLoading,         // Boolean\n  error,             // Error message or null\n\n  // Actions\n  signIn,            // (email, password) => Promise<void>\n  signUp,            // (data: SignUpData) => Promise<void>\n  signUpPasswordless, // (data) => Promise<{ success, error? }>\n  requestMagicLink,  // (email) => Promise<{ success, error? }>\n  verifyMagicLink,   // (token) => Promise<{ success, error? }>\n  signInWithGoogle,  // () => Promise<void>\n  signInWithPasskey, // () => Promise<void>\n  signOut,           // () => Promise<void>\n  resetPassword,     // (email) => Promise<void>\n  refreshMemberData, // () => Promise<void>\n  clearError,        // () => void\n} = useAkinAuth();\n```\n\n### Headless Auth Components\n\n#### LoginForm\n\nMulti-mode login form supporting passkeys, magic links, and passwords.\n\n```tsx\n<LoginForm onSuccess={onSuccess} onError={onError}>\n  {({\n    // Mode management\n    mode,              // 'passkey' | 'magic-link' | 'magic-link-sent' | 'password'\n    setMode,           // (mode) => void\n\n    // Form state\n    email, setEmail,\n    password, setPassword,\n    isLoading, error,\n\n    // Passkey\n    passkeySupported,  // Boolean - check before showing passkey option\n    handlePasskeyLogin,\n\n    // Magic link\n    handleMagicLink,\n    handleResendMagicLink,\n\n    // Password (if enabled)\n    handleSubmit,\n\n    // Google OAuth\n    handleGoogleLogin,\n\n    clearError,\n  }) => (\n    // Your JSX\n  )}\n</LoginForm>\n```\n\n#### SignupForm\n\nRegistration form with optional passwordless mode.\n\n```tsx\n<SignupForm\n  onSuccess={onSuccess}\n  onError={onError}\n  passwordless={true}  // Set to true for magic link signup\n>\n  {({\n    firstName, setFirstName,\n    lastName, setLastName,\n    email, setEmail,\n    password, setPassword,      // Only used if passwordless={false}\n    phone, setPhone,\n    termsAccepted, setTermsAccepted,\n    marketingOptIn, setMarketingOptIn,\n    isLoading, error,\n    handleSubmit,\n    handleGoogleSignup,\n    clearError,\n  }) => (\n    // Your JSX\n  )}\n</SignupForm>\n```\n\n#### MagicLinkForm\n\nStandalone magic link request form.\n\n```tsx\n<MagicLinkForm onSuccess={onSuccess}>\n  {({ email, setEmail, isLoading, error, success, handleSubmit }) => (\n    // Your JSX\n  )}\n</MagicLinkForm>\n```\n\n#### PhoneInput\n\nInternational phone number input with country selector.\n\n```tsx\nimport { PhoneInput } from '@akin-online/partner-sdk';\nimport 'react-phone-number-input/style.css';\n\n<PhoneInput\n  id=\"phone\"\n  value={phone}\n  onChange={setPhone}\n  defaultCountry=\"US\"\n  placeholder=\"Enter phone number\"\n  className=\"your-input-class\"\n  required\n/>\n```\n\n## Loyalty API\n\n### useAkinLoyalty Hook\n\n```typescript\nconst {\n  // State\n  points,           // Current points balance\n  tier,             // 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4'\n  tierConfig,       // Current tier configuration\n  tierProgress,     // Progress to next tier\n  transactions,     // Recent transactions\n  tierHistory,      // Tier change history\n  isLoading,        // Boolean\n  error,            // Error message or null\n\n  // Actions\n  refreshData,      // () => Promise<void>\n  getTransactionHistory, // (options?) => Promise<Transaction[]>\n  getTierHistory,   // (limit?) => Promise<TierHistory[]>\n} = useAkinLoyalty();\n```\n\n### Headless Loyalty Components\n\n#### TierCard\n\n```tsx\n<TierCard>\n  {({ tier, displayName, color, icon, points, loyaltyNumber, maxPerks }) => (\n    // Your JSX\n  )}\n</TierCard>\n```\n\n#### TierProgress\n\n```tsx\n<TierProgress>\n  {({\n    currentTier, currentTierName,\n    nextTier, nextTierName,\n    percentage, pointsNeeded,\n    totalStays, originStays,  // Note: originStays (not originPartnerStays)\n  }) => (\n    nextTierName ? (\n      <div>\n        <progress value={percentage} max={100} />\n        <p>{pointsNeeded} points to {nextTierName}</p>\n      </div>\n    ) : (\n      <p>You've reached the highest tier!</p>\n    )\n  )}\n</TierProgress>\n```\n\n#### TransactionList\n\n```tsx\n<TransactionList limit={10}>\n  {({ transactions, isLoading, hasMore, loadMore }) => (\n    // Your JSX\n  )}\n</TransactionList>\n```\n\n#### PointsDisplay\n\n```tsx\n<PointsDisplay>\n  {({ points, formattedPoints, isLoading }) => (\n    // Your JSX\n  )}\n</PointsDisplay>\n```\n\n#### TierRequirementsTable\n\nDisplay tier requirements in a table format. Perfect for \"how to qualify\" sections.\n\n```tsx\nimport { TierRequirementsTable } from '@akin-online/partner-sdk';\n\n<TierRequirementsTable partnerName=\"Haka\">\n  {({ tierConfigs, requirementKeys, getRequirementLabel, formatValue, getRequirement, isLoading }) => (\n    <table>\n      <thead>\n        <tr>\n          <th>Requirement</th>\n          {tierConfigs.map(tier => (\n            <th key={tier.id}>{tier.displayName}</th>\n          ))}\n        </tr>\n      </thead>\n      <tbody>\n        {requirementKeys.map(key => (\n          <tr key={key}>\n            <td>{getRequirementLabel(key)}</td>\n            {tierConfigs.map(tier => {\n              const req = getRequirement(tier, key);\n              return (\n                <td key={tier.id}>\n                  {req ? formatValue(Number(req.requirementValue), req.operator) : '—'}\n                </td>\n              );\n            })}\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  )}\n</TierRequirementsTable>\n```\n\n**Render Props:**\n- `tierConfigs` - Tier configs sorted by display order\n- `requirementKeys` - Unique requirement keys sorted by priority\n- `getRequirementLabel(key, displayName?)` - Get display label for a key\n- `formatValue(value, operator)` - Format value with operator (e.g., \"10+\")\n- `getRequirement(tier, key)` - Get requirement for tier/key\n- `partnerName` - Partner name used for dynamic labels\n- `isLoading` - Loading state\n\n#### SimpleTierCards\n\nDisplay tier cards with requirements. Great for tier overview sections.\n\n```tsx\nimport { SimpleTierCards } from '@akin-online/partner-sdk';\n\n<SimpleTierCards partnerName=\"Viajero\">\n  {({ tierConfigs, formatRequirement, getTextColor, isLoading }) => (\n    <div>\n      <h2 className=\"text-2xl font-bold text-center mb-6\">Viajero GO Tiers</h2>\n      <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 items-stretch\">\n        {tierConfigs.map((tier, index) => {\n          const bgColor = tier.tierColor || '#382108';\n          const textColor = getTextColor(bgColor);\n          return (\n            <div\n              key={tier.id}\n              style={{ backgroundColor: bgColor, color: textColor }}\n              className=\"p-5 rounded-xl flex flex-col h-full\"\n            >\n              {/* Tier icon */}\n              {tier.tierIcon && (\n                <div className=\"w-10 h-10 mb-3 rounded-full bg-white/20 flex items-center justify-center\">\n                  <img src={tier.tierIcon} alt=\"\" className=\"w-6 h-6\" />\n                </div>\n              )}\n              {/* Tier name */}\n              <h3 className=\"text-2xl font-bold\">{tier.displayName}</h3>\n              <span className=\"text-sm opacity-70 mb-3\">Tier {index + 1}</span>\n              {/* Requirements */}\n              <div className=\"mt-auto pt-3 border-t border-current/20 space-y-1\">\n                {tier.requirements?.filter(r => r.active).map(req => (\n                  <p key={req.id} className=\"text-xs opacity-80\">\n                    {formatRequirement(req)}\n                  </p>\n                ))}\n              </div>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  )}\n</SimpleTierCards>\n```\n\n**Render Props:**\n- `tierConfigs` - Tier configs sorted by display order\n- `formatRequirement(req)` - Format requirement (e.g., \"10+ Origin stays\")\n- `getTextColor(bgColor)` - Get contrasting text color for background\n- `isLoading` - Loading state\n\n#### TierBenefits\n\nDisplay rewards/benefits available at each tier. Fetched automatically via the loyalty provider.\n\n```tsx\nimport { TierBenefits } from '@akin-online/partner-sdk';\n\n<TierBenefits>\n  {({ tierBenefits, getTierConfig, getTextColor, isLoading }) => (\n    <div>\n      <h2 className=\"text-2xl font-bold text-center mb-6\">Tier Benefits</h2>\n      <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 items-stretch\">\n        {tierBenefits.map((benefit) => {\n          const config = getTierConfig(benefit.tierName);\n          const bgColor = config?.tierColor || '#382108';\n          const textColor = config?.tierTextColor || getTextColor(bgColor);\n          return (\n            <div\n              key={benefit.tierName}\n              style={{ backgroundColor: bgColor, color: textColor }}\n              className=\"p-5 rounded-xl flex flex-col h-full\"\n            >\n              <h3 className=\"text-xl font-bold mb-2\">{config?.displayName}</h3>\n              <div className=\"w-full h-px mb-3 opacity-30 bg-current\" />\n              {benefit.groups?.some(g => g.scope === 'PROPERTY') ? (\n                /* Grouped by property */\n                <div className=\"space-y-3 flex-1\">\n                  {benefit.groups.map((group) => (\n                    <div key={group.scope === 'ALL_PROPERTIES' ? 'all' : group.propertyId}>\n                      <h4 className=\"text-xs font-semibold uppercase tracking-wide mb-1 opacity-70\">\n                        {group.scope === 'ALL_PROPERTIES' ? 'All Properties' : group.propertyName}\n                      </h4>\n                      <ul className=\"space-y-1\">\n                        {group.rewardNames.map((name, i) => (\n                          <li key={i} className=\"text-sm flex items-start gap-2\">\n                            <span className=\"mt-0.5\">✓</span>\n                            <span>{name}</span>\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  ))}\n                </div>\n              ) : (\n                /* Flat list */\n                <ul className=\"space-y-1.5 flex-1\">\n                  {benefit.rewardNames.map((name, i) => (\n                    <li key={i} className=\"text-sm flex items-start gap-2\">\n                      <span className=\"mt-0.5\">✓</span>\n                      <span>{name}</span>\n                    </li>\n                  ))}\n                </ul>\n              )}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  )}\n</TierBenefits>\n```\n\n**Render Props:**\n- `tierBenefits` - Tier benefits sorted by display order, each with `tierName`, `rewardNames`, and optional `groups`\n- `tierConfigs` - Tier configs sorted by display order (for colors/names)\n- `getTierConfig(tierName)` - Get tier config for a given tier name\n- `getTextColor(bgColor)` - Get contrasting text color for background\n- `isLoading` - Loading state\n\n## Tier Label Utilities\n\nCentralized labels for tier requirement keys.\n\n```tsx\nimport { getLabel, TIER_LABELS, PARTNER_LABELS, KEY_PRIORITY, extractRequirementKeys } from '@akin-online/partner-sdk';\n\n// Get a label for a requirement key\ngetLabel('stays_member_origin');           // \"Origin stays\"\ngetLabel('stays_partner', 'Haka');         // \"Haka stays\"\ngetLabel('points', 'Origin', 'Custom');    // \"Custom\" (displayName override)\n\n// Static labels\nTIER_LABELS['points'];                     // \"AKIN points\"\nTIER_LABELS['stays_network'];              // \"Network stays\"\n\n// Dynamic labels (with partner name)\nPARTNER_LABELS['stays_partner']('Haka');   // \"Haka stays\"\nPARTNER_LABELS['achievement_tattoo']('Drifter'); // \"Drifter tattoo\"\n\n// Extract unique requirement keys from tier configs (sorted by priority)\nconst keys = extractRequirementKeys(tierConfigs);\n// ['stays_partner', 'stays_member_origin', 'points', ...]\n```\n\n**Available Labels:**\n- `stays_member_origin` → \"Origin stays\"\n- `stays_network` → \"Network stays\"\n- `stays_partner` → \"[Partner] stays\"\n- `nights_member_origin` → \"Origin nights\"\n- `nights_network` → \"Network nights\"\n- `nights_partner` → \"[Partner] nights\"\n- `points` → \"AKIN points\"\n- `network_points` → \"Network points\"\n- `spend` → \"Total spend\"\n- `referrals` → \"Referrals\"\n- `achievement_tattoo` → \"[Partner] tattoo\"\n- `achievement_invitation` → \"Invitation\"\n- `country_completion_mx` → \"Mexico complete\"\n- `country_completion_pe` → \"Peru complete\"\n- `country_completion_co` → \"Colombia complete\"\n\n## Types\n\n### AkinMember\n\n```typescript\ninterface AkinMember {\n  id: string;\n  email: string;\n  firstName: string;\n  lastName: string;\n  phone?: string;\n  points: number;\n  currentTier: 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4';\n  status: 'ACTIVE' | 'SUSPENDED' | 'DELETED';\n  loyaltyNumber: string;\n  vibePreference?: string;\n  perkPreferences?: string[];\n  createdAt: Date;\n  updatedAt: Date;\n}\n```\n\n### TierConfig\n\n```typescript\ninterface TierConfig {\n  id: string;\n  tierName: string;        // 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4'\n  displayName: string;     // 'Explorer' | 'Voyager' | etc.\n  displayOrder: number;\n  maxPerksAllowed: number;\n  tierColor?: string;\n  tierIcon?: string;\n  tierByline?: string;\n  eligibility?: string;    // Note: eligibility (not eligibilityText)\n  requirements?: TierRequirement[];  // Key/value tier requirements\n}\n```\n\n### TierRequirement\n\n```typescript\ninterface TierRequirement {\n  id: string;\n  tierConfigId: string;\n  requirementKey: string;      // e.g., 'stays_member_origin', 'points'\n  requirementValue: string;    // Threshold value\n  valueType: 'INT' | 'FLOAT' | 'DATE' | 'STRING';\n  operator: 'eq' | 'gt' | 'gte' | 'lt' | 'lte';\n  logicGroup: number;          // For AND/OR logic combinations\n  displayOrder: number;\n  displayName?: string;        // Custom display override\n  active: boolean;\n}\n```\n\n### LoyaltyTransaction\n\n```typescript\ninterface LoyaltyTransaction {\n  id: string;\n  memberId: string;\n  bookingId?: string;\n  pointsChange: number;\n  transactionType: 'EARNED' | 'SPENT' | 'EXPIRED' | 'ADJUSTED';\n  reason?: string;\n  description?: string;\n  balanceAfter: number;\n  createdAt: Date;\n}\n```\n\n### TierProgress\n\n```typescript\ninterface TierProgress {\n  currentTier: string;\n  currentTierName: string;\n  nextTier: string | null;\n  nextTierName: string | null;\n  percentage: number;\n  pointsNeeded: number;\n  totalStays: number;\n  originStays: number;  // Note: originStays (not originPartnerStays)\n}\n```\n\n## Environment Variables\n\n```bash\n# Required - Partner credentials\nNEXT_PUBLIC_AKIN_PARTNER_ID=your-partner-id\nNEXT_PUBLIC_AKIN_API_KEY=your-api-key\nNEXT_PUBLIC_AKIN_VERIFY_URL=https://yourdomain.com/auth/verify\n\n# Required - SDK infrastructure (provided by Akin during onboarding)\nNEXT_PUBLIC_AKIN_SDK_API_URL=https://api.akintravel.com/graphql\nNEXT_PUBLIC_AKIN_SDK_GIP_TENANT_ID=your-gip-tenant-id\nNEXT_PUBLIC_AKIN_SDK_FIREBASE_API_KEY=your-firebase-api-key\nNEXT_PUBLIC_AKIN_SDK_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com\nNEXT_PUBLIC_AKIN_SDK_FIREBASE_PROJECT_ID=your-firebase-project-id\nNEXT_PUBLIC_AKIN_SDK_FIREBASE_APP_ID=your-firebase-app-id\n\n# Optional - Environment-specific API URLs\nNEXT_PUBLIC_AKIN_SDK_STAGING_API_URL=https://staging-api.example.com/graphql\nNEXT_PUBLIC_AKIN_SDK_DEV_API_URL=http://localhost:4000/graphql\n```\n\n## Security Best Practices\n\n### API Key Handling\n\n**IMPORTANT:** Always use environment variables for your API key. Never commit API keys to source control.\n\n```tsx\n// ✅ CORRECT - Use environment variable\napiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY!\n\n// ❌ WRONG - Never hardcode API keys\napiKey: 'pk_live_abc123...'\n\n// ❌ WRONG - Never use fallback values in production\napiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY || 'demo-key'\n```\n\n### Debug Mode\n\nOnly enable debug mode in development:\n\n```tsx\ndebug: process.env.NODE_ENV === 'development'\n```\n\n### Environment File (.env.local)\n\nAll 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.\n\n```bash\n# Add to .gitignore\n.env.local\n.env*.local\n```\n\n### CORS\n\nThe SDK automatically handles CORS with the Akin API. If you need to allow additional origins, contact your Akin partner representative.\n\n## CSS Styling Notes\n\n### Link Color Override\n\nIf you have global link styles that override button text colors, use a more specific selector:\n\n```css\n/* ❌ This will override button text colors */\na {\n  @apply text-blue-600;\n}\n\n/* ✅ Use this instead - excludes elements with explicit text colors */\na:not([class*=\"text-\"]) {\n  @apply text-blue-600;\n}\n```\n\n### Phone Input Styles\n\nImport the phone input CSS:\n\n```tsx\nimport 'react-phone-number-input/style.css';\n```\n\n## Examples\n\n### Magic Link Verification Page\n\nCreate an `/auth/verify` route to handle magic link callbacks:\n\n```tsx\n'use client';\n\nimport { Suspense, useEffect, useState } from 'react';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport { useAkinAuth } from '@akin-online/partner-sdk';\n\nfunction VerifyContent() {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const { verifyMagicLink } = useAkinAuth();\n  const [state, setState] = useState<'loading' | 'success' | 'error'>('loading');\n  const [error, setError] = useState<string | null>(null);\n\n  useEffect(() => {\n    const token = searchParams.get('token');\n\n    if (!token) {\n      setError('Invalid verification link');\n      setState('error');\n      return;\n    }\n\n    const verify = async () => {\n      const result = await verifyMagicLink(token);\n\n      if (!result.success) {\n        setError(result.error || 'Verification failed');\n        setState('error');\n        return;\n      }\n\n      setState('success');\n      setTimeout(() => router.push('/rewards'), 1500);\n    };\n\n    verify();\n  }, [searchParams, verifyMagicLink, router]);\n\n  if (state === 'loading') return <p>Verifying...</p>;\n  if (state === 'success') return <p>Verified! Redirecting...</p>;\n  return <p>Error: {error}</p>;\n}\n\nexport default function VerifyPage() {\n  return (\n    <Suspense fallback={<p>Loading...</p>}>\n      <VerifyContent />\n    </Suspense>\n  );\n}\n```\n\n### Complete Passwordless Signup\n\n```tsx\n'use client';\n\nimport { useState } from 'react';\nimport { SignupForm, PhoneInput } from '@akin-online/partner-sdk';\nimport 'react-phone-number-input/style.css';\n\nexport default function SignupPage() {\n  const [signupComplete, setSignupComplete] = useState(false);\n  const [submittedEmail, setSubmittedEmail] = useState('');\n\n  if (signupComplete) {\n    return (\n      <div className=\"text-center\">\n        <h1>Check your email!</h1>\n        <p>We sent a verification link to {submittedEmail}</p>\n      </div>\n    );\n  }\n\n  return (\n    <SignupForm\n      passwordless\n      onSuccess={() => {}}\n      onError={(error) => console.error(error)}\n    >\n      {({\n        firstName, setFirstName,\n        lastName, setLastName,\n        email, setEmail,\n        phone, setPhone,\n        termsAccepted, setTermsAccepted,\n        marketingOptIn, setMarketingOptIn,\n        isLoading, error,\n        handleSubmit,\n        clearError,\n      }) => (\n        <form\n          onSubmit={(e) => {\n            e.preventDefault();\n            setSubmittedEmail(email);\n            handleSubmit(e).then(() => {\n              if (!error) setSignupComplete(true);\n            });\n          }}\n        >\n          <div className=\"grid grid-cols-2 gap-4\">\n            <input\n              value={firstName}\n              onChange={(e) => { setFirstName(e.target.value); clearError(); }}\n              placeholder=\"First name\"\n              required\n            />\n            <input\n              value={lastName}\n              onChange={(e) => { setLastName(e.target.value); clearError(); }}\n              placeholder=\"Last name\"\n              required\n            />\n          </div>\n\n          <input\n            type=\"email\"\n            value={email}\n            onChange={(e) => { setEmail(e.target.value); clearError(); }}\n            placeholder=\"Email\"\n            required\n          />\n\n          <PhoneInput\n            value={phone}\n            onChange={(value) => { setPhone(value); clearError(); }}\n            defaultCountry=\"US\"\n            placeholder=\"Phone number\"\n            required\n          />\n\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={termsAccepted}\n              onChange={(e) => setTermsAccepted(e.target.checked)}\n              required\n            />\n            I agree to the Terms of Service and Privacy Policy\n          </label>\n\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={marketingOptIn}\n              onChange={(e) => setMarketingOptIn(e.target.checked)}\n            />\n            Send me updates and offers\n          </label>\n\n          {error && <p className=\"error\">{error}</p>}\n\n          <button type=\"submit\" disabled={isLoading}>\n            {isLoading ? 'Creating account...' : 'Create Account'}\n          </button>\n        </form>\n      )}\n    </SignupForm>\n  );\n}\n```\n\n### Complete Login Page with Passkeys\n\n```tsx\n'use client';\n\nimport { useAkinAuth, LoginForm } from '@akin-online/partner-sdk';\nimport { useRouter } from 'next/navigation';\nimport { useEffect } from 'react';\n\nexport default function LoginPage() {\n  const router = useRouter();\n  const { isAuthenticated } = useAkinAuth();\n\n  useEffect(() => {\n    if (isAuthenticated) router.push('/rewards');\n  }, [isAuthenticated, router]);\n\n  return (\n    <div className=\"login-container\">\n      <h1>Welcome Back</h1>\n\n      <LoginForm\n        onSuccess={() => router.push('/rewards')}\n        onError={(error) => console.error(error)}\n      >\n        {({\n          mode, setMode,\n          email, setEmail,\n          isLoading, error,\n          passkeySupported,\n          handlePasskeyLogin,\n          handleMagicLink,\n          handleResendMagicLink,\n          clearError,\n        }) => (\n          <div>\n            {error && <p className=\"error\">{error}</p>}\n\n            {mode === 'passkey' && (\n              <div>\n                <button\n                  onClick={handlePasskeyLogin}\n                  disabled={!passkeySupported || isLoading}\n                >\n                  {isLoading ? 'Signing in...' : 'Sign in with Passkey'}\n                </button>\n                <button onClick={() => { clearError(); setMode('magic-link'); }}>\n                  Sign in with Email\n                </button>\n              </div>\n            )}\n\n            {mode === 'magic-link' && (\n              <div>\n                <input\n                  type=\"email\"\n                  value={email}\n                  onChange={(e) => { setEmail(e.target.value); clearError(); }}\n                  placeholder=\"you@example.com\"\n                />\n                <button\n                  onClick={handleMagicLink}\n                  disabled={isLoading || !email}\n                >\n                  {isLoading ? 'Sending...' : 'Send sign-in link'}\n                </button>\n                {passkeySupported && (\n                  <button onClick={() => { clearError(); setMode('passkey'); }}>\n                    ← Back to passkey sign-in\n                  </button>\n                )}\n              </div>\n            )}\n\n            {mode === 'magic-link-sent' && (\n              <div className=\"text-center\">\n                <h2>Check your email</h2>\n                <p>We sent a sign-in link to {email}</p>\n                <button onClick={handleResendMagicLink} disabled={isLoading}>\n                  {isLoading ? 'Resending...' : 'Resend link'}\n                </button>\n                <button onClick={() => { clearError(); setMode('magic-link'); }}>\n                  Try a different email\n                </button>\n              </div>\n            )}\n          </div>\n        )}\n      </LoginForm>\n    </div>\n  );\n}\n```\n\n### Complete Rewards Dashboard\n\n```tsx\n'use client';\n\nimport {\n  useAkinAuth,\n  useAkinLoyalty,\n  TierCard,\n  TierProgress,\n  TransactionList,\n  PointsDisplay,\n} from '@akin-online/partner-sdk';\nimport { useRouter } from 'next/navigation';\nimport { useEffect } from 'react';\n\nexport default function RewardsPage() {\n  const router = useRouter();\n  const { member, isAuthenticated, isLoading: authLoading, signOut } = useAkinAuth();\n  const { isLoading: loyaltyLoading } = useAkinLoyalty();\n\n  useEffect(() => {\n    if (!authLoading && !isAuthenticated) {\n      router.push('/login');\n    }\n  }, [isAuthenticated, authLoading, router]);\n\n  if (authLoading || loyaltyLoading) {\n    return <p>Loading...</p>;\n  }\n\n  if (!isAuthenticated) return null;\n\n  return (\n    <div className=\"rewards-container\">\n      <header>\n        <h1>Welcome back, {member?.firstName}!</h1>\n        <p>Loyalty #: {member?.loyaltyNumber}</p>\n        <button onClick={() => signOut().then(() => router.push('/'))}>\n          Sign Out\n        </button>\n      </header>\n\n      <div className=\"grid grid-cols-2 gap-4\">\n        <div className=\"card\">\n          <h2>Your Points</h2>\n          <PointsDisplay>\n            {({ formattedPoints }) => (\n              <p className=\"text-4xl font-bold\">{formattedPoints}</p>\n            )}\n          </PointsDisplay>\n        </div>\n\n        <div className=\"card\">\n          <h2>Current Tier</h2>\n          <TierCard>\n            {({ displayName, tier }) => (\n              <div>\n                <p className=\"text-2xl font-bold\">{displayName}</p>\n                <p className=\"text-sm\">Tier {tier.replace('TIER', '')}</p>\n              </div>\n            )}\n          </TierCard>\n        </div>\n      </div>\n\n      <div className=\"card\">\n        <h2>Progress to Next Tier</h2>\n        <TierProgress>\n          {({ currentTierName, nextTierName, percentage, pointsNeeded }) => (\n            nextTierName ? (\n              <div>\n                <div className=\"flex justify-between\">\n                  <span>{currentTierName}</span>\n                  <span>{nextTierName}</span>\n                </div>\n                <div className=\"progress-bar\">\n                  <div style={{ width: `${percentage}%` }} />\n                </div>\n                <p>{pointsNeeded.toLocaleString()} points to {nextTierName}</p>\n              </div>\n            ) : (\n              <p>You've reached the highest tier!</p>\n            )\n          )}\n        </TierProgress>\n      </div>\n\n      <div className=\"card\">\n        <h2>Recent Activity</h2>\n        <TransactionList limit={10}>\n          {({ transactions, isLoading, hasMore, loadMore }) => (\n            <div>\n              {transactions.length === 0 ? (\n                <p>No transactions yet</p>\n              ) : (\n                <>\n                  <ul>\n                    {transactions.map((tx) => (\n                      <li key={tx.id} className=\"flex justify-between py-3\">\n                        <div>\n                          <p>{tx.reason || tx.description || 'Transaction'}</p>\n                          <p className=\"text-sm text-gray-500\">\n                            {new Date(tx.createdAt).toLocaleDateString()}\n                          </p>\n                        </div>\n                        <span className={tx.pointsChange > 0 ? 'text-green-600' : 'text-red-600'}>\n                          {tx.pointsChange > 0 ? '+' : ''}{tx.pointsChange.toLocaleString()}\n                        </span>\n                      </li>\n                    ))}\n                  </ul>\n                  {hasMore && (\n                    <button onClick={loadMore} disabled={isLoading}>\n                      {isLoading ? 'Loading...' : 'Load More'}\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n          )}\n        </TransactionList>\n      </div>\n    </div>\n  );\n}\n```\n\n## Development / Examples\n\n### Viajero Demo\n\nA complete example implementation is available in the monorepo at `examples/viajero/`. This demonstrates a partner rewards site using the SDK with:\n\n- Login page with passkey and magic link authentication\n- Signup form with passwordless registration\n- Rewards dashboard with loyalty card, tier cards, tier benefits, upcoming stays, and activity statement\n- Full Tailwind CSS styling with shadcn/ui components\n\n**Running the demo:**\n\n```bash\n# From monorepo root\ncd examples/viajero\nnpm install\nnpm run dev\n```\n\nThe demo runs at **http://localhost:3003**\n\n**Demo structure:**\n```\nviajero/\n├── app/\n│   ├── layout.tsx      # AkinProvider setup\n│   ├── page.tsx        # Landing page\n│   ├── login/          # Login with passkey/magic link\n│   ├── signup/         # Passwordless signup\n│   ├── account/        # Authenticated rewards dashboard\n│   ├── settings/       # Account settings\n│   └── auth/verify/    # Magic link verification\n├── components/         # shadcn/ui components\n└── .env.local.example  # Environment variables template\n```\n\n### Local SDK Development\n\nWhen developing the SDK itself, the demo uses a file reference to the local package:\n\n```json\n{\n  \"dependencies\": {\n    \"@akin-travel/partner-sdk\": \"file:../../packages/partner-sdk\"\n  }\n}\n```\n\nAfter making changes to the SDK, rebuild and restart the demo:\n\n```bash\n# From monorepo root\nnpm run build -w @akin-online/partner-sdk\ncd examples/viajero && npm run dev\n```\n\n## Troubleshooting\n\n### \"Loading...\" stuck after passkey authentication\n\nEnsure you're using the latest SDK version. The `signInWithPasskey` function properly resets loading state in the `finally` block.\n\n### Button text color not showing\n\nCheck for global CSS rules that override text colors. See [CSS Styling Notes](#css-styling-notes).\n\n### Phone input not styled\n\nImport the phone input CSS: `import 'react-phone-number-input/style.css';`\n\n### Magic link not working\n\n1. Verify your `NEXT_PUBLIC_AKIN_API_KEY` is set correctly\n2. Check that your domain is in the allowed origins for your API key\n3. Ensure you have an `/auth/verify` route to handle the callback\n\n### GraphQL errors\n\nIf you see errors about field types or non-existent fields, ensure you're using the latest SDK version. Common issues:\n- Use `String!` (not `ID!`) for member/partner IDs in queries\n- Use `originStays` (not `originPartnerStays`)\n- Use `eligibility` (not `eligibilityText`)\n\n## Support\n\nFor questions or issues, contact your Akin partner representative or email support@akintravel.com\n","readmeFilename":"README.md"}