@akin-travel/partner-sdk 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +1204 -0
  2. package/dist/account/components/AccountInfoSection.d.ts +46 -0
  3. package/dist/account/components/AccountInfoSection.d.ts.map +1 -0
  4. package/dist/account/components/AccountInfoSection.js +52 -0
  5. package/dist/account/components/AccountInfoSection.js.map +1 -0
  6. package/dist/account/components/NotificationPreferencesSection.d.ts +40 -0
  7. package/dist/account/components/NotificationPreferencesSection.d.ts.map +1 -0
  8. package/dist/account/components/NotificationPreferencesSection.js +116 -0
  9. package/dist/account/components/NotificationPreferencesSection.js.map +1 -0
  10. package/dist/account/components/PasskeySection.d.ts +49 -0
  11. package/dist/account/components/PasskeySection.d.ts.map +1 -0
  12. package/dist/account/components/PasskeySection.js +298 -0
  13. package/dist/account/components/PasskeySection.js.map +1 -0
  14. package/dist/account/hooks/useAccountForm.d.ts +23 -0
  15. package/dist/account/hooks/useAccountForm.d.ts.map +1 -0
  16. package/dist/account/hooks/useAccountForm.js +133 -0
  17. package/dist/account/hooks/useAccountForm.js.map +1 -0
  18. package/dist/account/index.d.ts +10 -0
  19. package/dist/account/index.d.ts.map +1 -0
  20. package/dist/account/index.js +21 -0
  21. package/dist/account/index.js.map +1 -0
  22. package/dist/auth/AkinAuthProvider.d.ts +31 -0
  23. package/dist/auth/AkinAuthProvider.d.ts.map +1 -0
  24. package/dist/auth/AkinAuthProvider.js +632 -0
  25. package/dist/auth/AkinAuthProvider.js.map +1 -0
  26. package/dist/auth/components/LoginForm.d.ts +63 -0
  27. package/dist/auth/components/LoginForm.d.ts.map +1 -0
  28. package/dist/auth/components/LoginForm.js +230 -0
  29. package/dist/auth/components/LoginForm.js.map +1 -0
  30. package/dist/auth/components/MagicLinkForm.d.ts +41 -0
  31. package/dist/auth/components/MagicLinkForm.d.ts.map +1 -0
  32. package/dist/auth/components/MagicLinkForm.js +88 -0
  33. package/dist/auth/components/MagicLinkForm.js.map +1 -0
  34. package/dist/auth/components/RequireAuth.d.ts +62 -0
  35. package/dist/auth/components/RequireAuth.d.ts.map +1 -0
  36. package/dist/auth/components/RequireAuth.js +63 -0
  37. package/dist/auth/components/RequireAuth.js.map +1 -0
  38. package/dist/auth/components/RequireGuest.d.ts +60 -0
  39. package/dist/auth/components/RequireGuest.d.ts.map +1 -0
  40. package/dist/auth/components/RequireGuest.js +64 -0
  41. package/dist/auth/components/RequireGuest.js.map +1 -0
  42. package/dist/auth/components/SignupForm.d.ts +45 -0
  43. package/dist/auth/components/SignupForm.d.ts.map +1 -0
  44. package/dist/auth/components/SignupForm.js +167 -0
  45. package/dist/auth/components/SignupForm.js.map +1 -0
  46. package/dist/auth/index.d.ts +10 -0
  47. package/dist/auth/index.d.ts.map +1 -0
  48. package/dist/auth/index.js +21 -0
  49. package/dist/auth/index.js.map +1 -0
  50. package/dist/client.d.ts +17 -0
  51. package/dist/client.d.ts.map +1 -0
  52. package/dist/client.js +136 -0
  53. package/dist/client.js.map +1 -0
  54. package/dist/components/PhoneInput.d.ts +62 -0
  55. package/dist/components/PhoneInput.d.ts.map +1 -0
  56. package/dist/components/PhoneInput.js +65 -0
  57. package/dist/components/PhoneInput.js.map +1 -0
  58. package/dist/components/index.d.ts +2 -0
  59. package/dist/components/index.d.ts.map +1 -0
  60. package/dist/components/index.js +9 -0
  61. package/dist/components/index.js.map +1 -0
  62. package/dist/config.d.ts +111 -0
  63. package/dist/config.d.ts.map +1 -0
  64. package/dist/config.js +56 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/constants/preferences.d.ts +22 -0
  67. package/dist/constants/preferences.d.ts.map +1 -0
  68. package/dist/constants/preferences.js +52 -0
  69. package/dist/constants/preferences.js.map +1 -0
  70. package/dist/currency/CurrencyProvider.d.ts +46 -0
  71. package/dist/currency/CurrencyProvider.d.ts.map +1 -0
  72. package/dist/currency/CurrencyProvider.js +145 -0
  73. package/dist/currency/CurrencyProvider.js.map +1 -0
  74. package/dist/currency/components/CurrencySelector.d.ts +43 -0
  75. package/dist/currency/components/CurrencySelector.d.ts.map +1 -0
  76. package/dist/currency/components/CurrencySelector.js +58 -0
  77. package/dist/currency/components/CurrencySelector.js.map +1 -0
  78. package/dist/currency/exchangeRates.d.ts +40 -0
  79. package/dist/currency/exchangeRates.d.ts.map +1 -0
  80. package/dist/currency/exchangeRates.js +90 -0
  81. package/dist/currency/exchangeRates.js.map +1 -0
  82. package/dist/currency/index.d.ts +6 -0
  83. package/dist/currency/index.d.ts.map +1 -0
  84. package/dist/currency/index.js +20 -0
  85. package/dist/currency/index.js.map +1 -0
  86. package/dist/header/components/CurrencyAccordion.d.ts +45 -0
  87. package/dist/header/components/CurrencyAccordion.d.ts.map +1 -0
  88. package/dist/header/components/CurrencyAccordion.js +54 -0
  89. package/dist/header/components/CurrencyAccordion.js.map +1 -0
  90. package/dist/header/components/HeaderMenu.d.ts +49 -0
  91. package/dist/header/components/HeaderMenu.d.ts.map +1 -0
  92. package/dist/header/components/HeaderMenu.js +95 -0
  93. package/dist/header/components/HeaderMenu.js.map +1 -0
  94. package/dist/header/components/LanguageAccordion.d.ts +45 -0
  95. package/dist/header/components/LanguageAccordion.d.ts.map +1 -0
  96. package/dist/header/components/LanguageAccordion.js +54 -0
  97. package/dist/header/components/LanguageAccordion.js.map +1 -0
  98. package/dist/header/components/UserAvatar.d.ts +26 -0
  99. package/dist/header/components/UserAvatar.d.ts.map +1 -0
  100. package/dist/header/components/UserAvatar.js +46 -0
  101. package/dist/header/components/UserAvatar.js.map +1 -0
  102. package/dist/header/index.d.ts +10 -0
  103. package/dist/header/index.d.ts.map +1 -0
  104. package/dist/header/index.js +13 -0
  105. package/dist/header/index.js.map +1 -0
  106. package/dist/i18n/I18nProvider.d.ts +57 -0
  107. package/dist/i18n/I18nProvider.d.ts.map +1 -0
  108. package/dist/i18n/I18nProvider.js +205 -0
  109. package/dist/i18n/I18nProvider.js.map +1 -0
  110. package/dist/i18n/components/LanguageSelector.d.ts +43 -0
  111. package/dist/i18n/components/LanguageSelector.d.ts.map +1 -0
  112. package/dist/i18n/components/LanguageSelector.js +57 -0
  113. package/dist/i18n/components/LanguageSelector.js.map +1 -0
  114. package/dist/i18n/index.d.ts +5 -0
  115. package/dist/i18n/index.d.ts.map +1 -0
  116. package/dist/i18n/index.js +14 -0
  117. package/dist/i18n/index.js.map +1 -0
  118. package/dist/i18n/translations/en.json +283 -0
  119. package/dist/i18n/translations/es.json +283 -0
  120. package/dist/index.d.ts +81 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +191 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/loyalty/AkinLoyaltyProvider.d.ts +18 -0
  125. package/dist/loyalty/AkinLoyaltyProvider.d.ts.map +1 -0
  126. package/dist/loyalty/AkinLoyaltyProvider.js +399 -0
  127. package/dist/loyalty/AkinLoyaltyProvider.js.map +1 -0
  128. package/dist/loyalty/components/LoyaltyCard.d.ts +48 -0
  129. package/dist/loyalty/components/LoyaltyCard.d.ts.map +1 -0
  130. package/dist/loyalty/components/LoyaltyCard.js +140 -0
  131. package/dist/loyalty/components/LoyaltyCard.js.map +1 -0
  132. package/dist/loyalty/components/PointsDisplay.d.ts +40 -0
  133. package/dist/loyalty/components/PointsDisplay.d.ts.map +1 -0
  134. package/dist/loyalty/components/PointsDisplay.js +32 -0
  135. package/dist/loyalty/components/PointsDisplay.js.map +1 -0
  136. package/dist/loyalty/components/PreviousStays.d.ts +59 -0
  137. package/dist/loyalty/components/PreviousStays.d.ts.map +1 -0
  138. package/dist/loyalty/components/PreviousStays.js +101 -0
  139. package/dist/loyalty/components/PreviousStays.js.map +1 -0
  140. package/dist/loyalty/components/SimpleTierCards.d.ts +51 -0
  141. package/dist/loyalty/components/SimpleTierCards.d.ts.map +1 -0
  142. package/dist/loyalty/components/SimpleTierCards.js +96 -0
  143. package/dist/loyalty/components/SimpleTierCards.js.map +1 -0
  144. package/dist/loyalty/components/TierCard.d.ts +30 -0
  145. package/dist/loyalty/components/TierCard.d.ts.map +1 -0
  146. package/dist/loyalty/components/TierCard.js +41 -0
  147. package/dist/loyalty/components/TierCard.js.map +1 -0
  148. package/dist/loyalty/components/TierProgress.d.ts +32 -0
  149. package/dist/loyalty/components/TierProgress.d.ts.map +1 -0
  150. package/dist/loyalty/components/TierProgress.js +41 -0
  151. package/dist/loyalty/components/TierProgress.js.map +1 -0
  152. package/dist/loyalty/components/TierRequirementsTable.d.ts +54 -0
  153. package/dist/loyalty/components/TierRequirementsTable.d.ts.map +1 -0
  154. package/dist/loyalty/components/TierRequirementsTable.js +104 -0
  155. package/dist/loyalty/components/TierRequirementsTable.js.map +1 -0
  156. package/dist/loyalty/components/TransactionList.d.ts +44 -0
  157. package/dist/loyalty/components/TransactionList.d.ts.map +1 -0
  158. package/dist/loyalty/components/TransactionList.js +112 -0
  159. package/dist/loyalty/components/TransactionList.js.map +1 -0
  160. package/dist/loyalty/components/UpcomingStays.d.ts +49 -0
  161. package/dist/loyalty/components/UpcomingStays.d.ts.map +1 -0
  162. package/dist/loyalty/components/UpcomingStays.js +60 -0
  163. package/dist/loyalty/components/UpcomingStays.js.map +1 -0
  164. package/dist/loyalty/index.d.ts +12 -0
  165. package/dist/loyalty/index.d.ts.map +1 -0
  166. package/dist/loyalty/index.js +27 -0
  167. package/dist/loyalty/index.js.map +1 -0
  168. package/dist/types/account.d.ts +108 -0
  169. package/dist/types/account.d.ts.map +1 -0
  170. package/dist/types/account.js +3 -0
  171. package/dist/types/account.js.map +1 -0
  172. package/dist/types/auth.d.ts +205 -0
  173. package/dist/types/auth.d.ts.map +1 -0
  174. package/dist/types/auth.js +3 -0
  175. package/dist/types/auth.js.map +1 -0
  176. package/dist/types/currency.d.ts +102 -0
  177. package/dist/types/currency.d.ts.map +1 -0
  178. package/dist/types/currency.js +3 -0
  179. package/dist/types/currency.js.map +1 -0
  180. package/dist/types/header.d.ts +105 -0
  181. package/dist/types/header.d.ts.map +1 -0
  182. package/dist/types/header.js +3 -0
  183. package/dist/types/header.js.map +1 -0
  184. package/dist/types/i18n.d.ts +90 -0
  185. package/dist/types/i18n.d.ts.map +1 -0
  186. package/dist/types/i18n.js +3 -0
  187. package/dist/types/i18n.js.map +1 -0
  188. package/dist/types/index.d.ts +3 -0
  189. package/dist/types/index.d.ts.map +1 -0
  190. package/dist/types/index.js +3 -0
  191. package/dist/types/index.js.map +1 -0
  192. package/dist/types/loyalty.d.ts +312 -0
  193. package/dist/types/loyalty.d.ts.map +1 -0
  194. package/dist/types/loyalty.js +3 -0
  195. package/dist/types/loyalty.js.map +1 -0
  196. package/dist/utils/index.d.ts +2 -0
  197. package/dist/utils/index.d.ts.map +1 -0
  198. package/dist/utils/index.js +10 -0
  199. package/dist/utils/index.js.map +1 -0
  200. package/dist/utils/tierLabels.d.ts +27 -0
  201. package/dist/utils/tierLabels.d.ts.map +1 -0
  202. package/dist/utils/tierLabels.js +110 -0
  203. package/dist/utils/tierLabels.js.map +1 -0
  204. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,1204 @@
1
+ # @akin-online/partner-sdk
2
+
3
+ SDK for third-party partners to integrate Akin auth and loyalty services into their React applications.
4
+
5
+ ## Features
6
+
7
+ - **Authentication** - Magic links (passwordless), passkeys (WebAuthn), Google OAuth
8
+ - **Loyalty** - Tier display, points balance, transaction history, progress tracking
9
+ - **Tier Requirements** - Flexible key/value qualification rules with display utilities
10
+ - **Headless Components** - Full control over your UI with render props
11
+ - **TypeScript** - Full type support
12
+ - **Phone Input** - International phone number input with validation
13
+
14
+ ## Getting Access
15
+
16
+ The `@akin-online/partner-sdk` is a **private npm package** available to authorized Akin partners.
17
+
18
+ ### Partner Onboarding
19
+
20
+ Contact your Akin partner representative or email partners@akintravel.com to request access. You'll receive the following via secure channel:
21
+
22
+ | Credential | Purpose |
23
+ |------------|---------|
24
+ | **npm access token** | For installing the private `@akin-online/partner-sdk` package |
25
+ | **Partner ID** | Your unique identifier in the Akin system |
26
+ | **API key** | For authenticating with the Akin API |
27
+ | **SDK environment variables** | Firebase/GIP credentials and API URLs (`NEXT_PUBLIC_AKIN_SDK_*`) |
28
+
29
+ ### Configure npm Access
30
+
31
+ Add the npm access token to your `.npmrc` file:
32
+
33
+ ```bash
34
+ # ~/.npmrc or project .npmrc
35
+ //registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN
36
+ ```
37
+
38
+ **Security Note:** Never commit your `.npmrc` file with tokens to source control. Add it to `.gitignore`.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ npm install @akin-online/partner-sdk
44
+ ```
45
+
46
+ ### Peer Dependencies
47
+
48
+ ```bash
49
+ npm install react-phone-number-input
50
+ ```
51
+
52
+ ## Quick Start
53
+
54
+ ### 1. Wrap your app with AkinProvider
55
+
56
+ ```tsx
57
+ // app/layout.tsx or _app.tsx
58
+ import { AkinProvider } from '@akin-online/partner-sdk';
59
+
60
+ export default function RootLayout({ children }) {
61
+ return (
62
+ <AkinProvider
63
+ config={{
64
+ partnerId: process.env.NEXT_PUBLIC_AKIN_PARTNER_ID!,
65
+ apiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY!,
66
+ environment: 'production',
67
+ debug: false, // Enable for development troubleshooting
68
+ }}
69
+ >
70
+ {children}
71
+ </AkinProvider>
72
+ );
73
+ }
74
+ ```
75
+
76
+ ### 2. Use auth hooks
77
+
78
+ ```tsx
79
+ import { useAkinAuth } from '@akin-online/partner-sdk';
80
+
81
+ function Profile() {
82
+ const { member, isAuthenticated, signOut } = useAkinAuth();
83
+
84
+ if (!isAuthenticated) {
85
+ return <p>Please sign in</p>;
86
+ }
87
+
88
+ return (
89
+ <div>
90
+ <h1>Welcome, {member?.firstName}!</h1>
91
+ <p>Loyalty #: {member?.loyaltyNumber}</p>
92
+ <button onClick={signOut}>Sign Out</button>
93
+ </div>
94
+ );
95
+ }
96
+ ```
97
+
98
+ ### 3. Use headless components for custom UI
99
+
100
+ ```tsx
101
+ import { LoginForm, TierCard, PointsDisplay } from '@akin-online/partner-sdk';
102
+
103
+ function LoginPage() {
104
+ return (
105
+ <LoginForm onSuccess={() => router.push('/rewards')}>
106
+ {({
107
+ mode,
108
+ setMode,
109
+ email,
110
+ setEmail,
111
+ isLoading,
112
+ error,
113
+ passkeySupported,
114
+ handlePasskeyLogin,
115
+ handleMagicLink,
116
+ clearError,
117
+ }) => (
118
+ <div className="your-styles">
119
+ {/* Passkey login (recommended default) */}
120
+ {mode === 'passkey' && (
121
+ <>
122
+ <button onClick={handlePasskeyLogin} disabled={!passkeySupported || isLoading}>
123
+ Sign in with Passkey
124
+ </button>
125
+ <button onClick={() => setMode('magic-link')}>
126
+ Sign in with Email
127
+ </button>
128
+ </>
129
+ )}
130
+
131
+ {/* Magic link (email) login */}
132
+ {mode === 'magic-link' && (
133
+ <>
134
+ <input
135
+ type="email"
136
+ value={email}
137
+ onChange={(e) => setEmail(e.target.value)}
138
+ placeholder="Email"
139
+ />
140
+ <button onClick={handleMagicLink} disabled={isLoading || !email}>
141
+ Send sign-in link
142
+ </button>
143
+ </>
144
+ )}
145
+
146
+ {/* Magic link sent confirmation */}
147
+ {mode === 'magic-link-sent' && (
148
+ <p>Check your email for a sign-in link!</p>
149
+ )}
150
+
151
+ {error && <p className="error">{error}</p>}
152
+ </div>
153
+ )}
154
+ </LoginForm>
155
+ );
156
+ }
157
+ ```
158
+
159
+ ## Configuration
160
+
161
+ ```typescript
162
+ interface AkinSDKConfig {
163
+ // Required
164
+ partnerId: string; // Your partner ID (provided by Akin)
165
+ apiKey: string; // Your API key (provided by Akin)
166
+
167
+ // Optional - defaults to NEXT_PUBLIC_AKIN_SDK_* env vars
168
+ apiUrl?: string; // API endpoint (default: NEXT_PUBLIC_AKIN_SDK_API_URL)
169
+ environment?: 'production' | 'staging' | 'development';
170
+ debug?: boolean; // Enable console logging (default: false)
171
+ firebase?: { // Firebase/GIP config (default: NEXT_PUBLIC_AKIN_SDK_FIREBASE_* env vars)
172
+ apiKey: string;
173
+ authDomain: string;
174
+ projectId: string;
175
+ appId: string;
176
+ };
177
+ gipTenantId?: string; // GIP tenant ID (default: NEXT_PUBLIC_AKIN_SDK_GIP_TENANT_ID)
178
+ }
179
+ ```
180
+
181
+ ## Authentication Methods
182
+
183
+ The SDK supports three passwordless authentication methods:
184
+
185
+ ### 1. Passkeys (WebAuthn) - Recommended
186
+
187
+ Biometric/device authentication. Most secure and user-friendly.
188
+
189
+ ```tsx
190
+ const { signInWithPasskey } = useAkinAuth();
191
+
192
+ // Check if passkeys are supported
193
+ if (passkeySupported) {
194
+ await signInWithPasskey();
195
+ }
196
+ ```
197
+
198
+ ### 2. Magic Links (Email)
199
+
200
+ Passwordless email verification links.
201
+
202
+ ```tsx
203
+ const { requestMagicLink, verifyMagicLink } = useAkinAuth();
204
+
205
+ // Request a magic link
206
+ await requestMagicLink(email);
207
+
208
+ // Verify the token (on your /auth/verify page)
209
+ const result = await verifyMagicLink(token);
210
+ if (result.success) {
211
+ // User is authenticated
212
+ }
213
+ ```
214
+
215
+ ### 3. Google OAuth
216
+
217
+ Social login with Google.
218
+
219
+ ```tsx
220
+ const { signInWithGoogle } = useAkinAuth();
221
+
222
+ await signInWithGoogle();
223
+ ```
224
+
225
+ ## Auth API
226
+
227
+ ### useAkinAuth Hook
228
+
229
+ ```typescript
230
+ const {
231
+ // State
232
+ user, // Firebase user object
233
+ member, // Akin member data (points, tier, etc.)
234
+ isAuthenticated, // Boolean
235
+ isLoading, // Boolean
236
+ error, // Error message or null
237
+
238
+ // Actions
239
+ signIn, // (email, password) => Promise<void>
240
+ signUp, // (data: SignUpData) => Promise<void>
241
+ signUpPasswordless, // (data) => Promise<{ success, error? }>
242
+ requestMagicLink, // (email) => Promise<{ success, error? }>
243
+ verifyMagicLink, // (token) => Promise<{ success, error? }>
244
+ signInWithGoogle, // () => Promise<void>
245
+ signInWithPasskey, // () => Promise<void>
246
+ signOut, // () => Promise<void>
247
+ resetPassword, // (email) => Promise<void>
248
+ refreshMemberData, // () => Promise<void>
249
+ clearError, // () => void
250
+ } = useAkinAuth();
251
+ ```
252
+
253
+ ### Headless Auth Components
254
+
255
+ #### LoginForm
256
+
257
+ Multi-mode login form supporting passkeys, magic links, and passwords.
258
+
259
+ ```tsx
260
+ <LoginForm onSuccess={onSuccess} onError={onError}>
261
+ {({
262
+ // Mode management
263
+ mode, // 'passkey' | 'magic-link' | 'magic-link-sent' | 'password'
264
+ setMode, // (mode) => void
265
+
266
+ // Form state
267
+ email, setEmail,
268
+ password, setPassword,
269
+ isLoading, error,
270
+
271
+ // Passkey
272
+ passkeySupported, // Boolean - check before showing passkey option
273
+ handlePasskeyLogin,
274
+
275
+ // Magic link
276
+ handleMagicLink,
277
+ handleResendMagicLink,
278
+
279
+ // Password (if enabled)
280
+ handleSubmit,
281
+
282
+ // Google OAuth
283
+ handleGoogleLogin,
284
+
285
+ clearError,
286
+ }) => (
287
+ // Your JSX
288
+ )}
289
+ </LoginForm>
290
+ ```
291
+
292
+ #### SignupForm
293
+
294
+ Registration form with optional passwordless mode.
295
+
296
+ ```tsx
297
+ <SignupForm
298
+ onSuccess={onSuccess}
299
+ onError={onError}
300
+ passwordless={true} // Set to true for magic link signup
301
+ >
302
+ {({
303
+ firstName, setFirstName,
304
+ lastName, setLastName,
305
+ email, setEmail,
306
+ password, setPassword, // Only used if passwordless={false}
307
+ phone, setPhone,
308
+ termsAccepted, setTermsAccepted,
309
+ marketingOptIn, setMarketingOptIn,
310
+ isLoading, error,
311
+ handleSubmit,
312
+ handleGoogleSignup,
313
+ clearError,
314
+ }) => (
315
+ // Your JSX
316
+ )}
317
+ </SignupForm>
318
+ ```
319
+
320
+ #### MagicLinkForm
321
+
322
+ Standalone magic link request form.
323
+
324
+ ```tsx
325
+ <MagicLinkForm onSuccess={onSuccess}>
326
+ {({ email, setEmail, isLoading, error, success, handleSubmit }) => (
327
+ // Your JSX
328
+ )}
329
+ </MagicLinkForm>
330
+ ```
331
+
332
+ #### PhoneInput
333
+
334
+ International phone number input with country selector.
335
+
336
+ ```tsx
337
+ import { PhoneInput } from '@akin-online/partner-sdk';
338
+ import 'react-phone-number-input/style.css';
339
+
340
+ <PhoneInput
341
+ id="phone"
342
+ value={phone}
343
+ onChange={setPhone}
344
+ defaultCountry="US"
345
+ placeholder="Enter phone number"
346
+ className="your-input-class"
347
+ required
348
+ />
349
+ ```
350
+
351
+ ## Loyalty API
352
+
353
+ ### useAkinLoyalty Hook
354
+
355
+ ```typescript
356
+ const {
357
+ // State
358
+ points, // Current points balance
359
+ tier, // 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4'
360
+ tierConfig, // Current tier configuration
361
+ tierProgress, // Progress to next tier
362
+ transactions, // Recent transactions
363
+ tierHistory, // Tier change history
364
+ isLoading, // Boolean
365
+ error, // Error message or null
366
+
367
+ // Actions
368
+ refreshData, // () => Promise<void>
369
+ getTransactionHistory, // (options?) => Promise<Transaction[]>
370
+ getTierHistory, // (limit?) => Promise<TierHistory[]>
371
+ } = useAkinLoyalty();
372
+ ```
373
+
374
+ ### Headless Loyalty Components
375
+
376
+ #### TierCard
377
+
378
+ ```tsx
379
+ <TierCard>
380
+ {({ tier, displayName, color, icon, points, loyaltyNumber, maxPerks }) => (
381
+ // Your JSX
382
+ )}
383
+ </TierCard>
384
+ ```
385
+
386
+ #### TierProgress
387
+
388
+ ```tsx
389
+ <TierProgress>
390
+ {({
391
+ currentTier, currentTierName,
392
+ nextTier, nextTierName,
393
+ percentage, pointsNeeded,
394
+ totalStays, originStays, // Note: originStays (not originPartnerStays)
395
+ }) => (
396
+ nextTierName ? (
397
+ <div>
398
+ <progress value={percentage} max={100} />
399
+ <p>{pointsNeeded} points to {nextTierName}</p>
400
+ </div>
401
+ ) : (
402
+ <p>You've reached the highest tier!</p>
403
+ )
404
+ )}
405
+ </TierProgress>
406
+ ```
407
+
408
+ #### TransactionList
409
+
410
+ ```tsx
411
+ <TransactionList limit={10}>
412
+ {({ transactions, isLoading, hasMore, loadMore }) => (
413
+ // Your JSX
414
+ )}
415
+ </TransactionList>
416
+ ```
417
+
418
+ #### PointsDisplay
419
+
420
+ ```tsx
421
+ <PointsDisplay>
422
+ {({ points, formattedPoints, isLoading }) => (
423
+ // Your JSX
424
+ )}
425
+ </PointsDisplay>
426
+ ```
427
+
428
+ #### TierRequirementsTable
429
+
430
+ Display tier requirements in a table format. Perfect for "how to qualify" sections.
431
+
432
+ ```tsx
433
+ import { TierRequirementsTable } from '@akin-online/partner-sdk';
434
+
435
+ <TierRequirementsTable partnerName="Haka">
436
+ {({ tierConfigs, requirementKeys, getRequirementLabel, formatValue, getRequirement, isLoading }) => (
437
+ <table>
438
+ <thead>
439
+ <tr>
440
+ <th>Requirement</th>
441
+ {tierConfigs.map(tier => (
442
+ <th key={tier.id}>{tier.displayName}</th>
443
+ ))}
444
+ </tr>
445
+ </thead>
446
+ <tbody>
447
+ {requirementKeys.map(key => (
448
+ <tr key={key}>
449
+ <td>{getRequirementLabel(key)}</td>
450
+ {tierConfigs.map(tier => {
451
+ const req = getRequirement(tier, key);
452
+ return (
453
+ <td key={tier.id}>
454
+ {req ? formatValue(Number(req.requirementValue), req.operator) : '—'}
455
+ </td>
456
+ );
457
+ })}
458
+ </tr>
459
+ ))}
460
+ </tbody>
461
+ </table>
462
+ )}
463
+ </TierRequirementsTable>
464
+ ```
465
+
466
+ **Render Props:**
467
+ - `tierConfigs` - Tier configs sorted by display order
468
+ - `requirementKeys` - Unique requirement keys sorted by priority
469
+ - `getRequirementLabel(key, displayName?)` - Get display label for a key
470
+ - `formatValue(value, operator)` - Format value with operator (e.g., "10+")
471
+ - `getRequirement(tier, key)` - Get requirement for tier/key
472
+ - `partnerName` - Partner name used for dynamic labels
473
+ - `isLoading` - Loading state
474
+
475
+ #### SimpleTierCards
476
+
477
+ Display tier cards with requirements. Great for tier overview sections.
478
+
479
+ ```tsx
480
+ import { SimpleTierCards } from '@akin-online/partner-sdk';
481
+
482
+ <SimpleTierCards partnerName="Haka">
483
+ {({ tierConfigs, formatRequirement, getTextColor, isLoading }) => (
484
+ <div className="grid grid-cols-4 gap-4">
485
+ {tierConfigs.map((tier, index) => (
486
+ <div
487
+ key={tier.id}
488
+ style={{
489
+ backgroundColor: tier.tierColor || '#382108',
490
+ color: getTextColor(tier.tierColor || '#382108'),
491
+ }}
492
+ className="p-4 rounded-lg"
493
+ >
494
+ <h3 className="text-2xl font-bold">{tier.displayName}</h3>
495
+ <span className="text-sm">Tier {index + 1}</span>
496
+ {tier.requirements?.filter(r => r.active).map(req => (
497
+ <p key={req.id} className="text-xs opacity-80">
498
+ {formatRequirement(req)}
499
+ </p>
500
+ ))}
501
+ </div>
502
+ ))}
503
+ </div>
504
+ )}
505
+ </SimpleTierCards>
506
+ ```
507
+
508
+ **Render Props:**
509
+ - `tierConfigs` - Tier configs sorted by display order
510
+ - `formatRequirement(req)` - Format requirement (e.g., "10+ Origin stays")
511
+ - `getTextColor(bgColor)` - Get contrasting text color for background
512
+ - `isLoading` - Loading state
513
+
514
+ ## Tier Label Utilities
515
+
516
+ Centralized labels for tier requirement keys.
517
+
518
+ ```tsx
519
+ import { getLabel, TIER_LABELS, PARTNER_LABELS, KEY_PRIORITY, extractRequirementKeys } from '@akin-online/partner-sdk';
520
+
521
+ // Get a label for a requirement key
522
+ getLabel('stays_member_origin'); // "Origin stays"
523
+ getLabel('stays_partner', 'Haka'); // "Haka stays"
524
+ getLabel('points', 'Origin', 'Custom'); // "Custom" (displayName override)
525
+
526
+ // Static labels
527
+ TIER_LABELS['points']; // "AKIN points"
528
+ TIER_LABELS['stays_network']; // "Network stays"
529
+
530
+ // Dynamic labels (with partner name)
531
+ PARTNER_LABELS['stays_partner']('Haka'); // "Haka stays"
532
+ PARTNER_LABELS['achievement_tattoo']('Drifter'); // "Drifter tattoo"
533
+
534
+ // Extract unique requirement keys from tier configs (sorted by priority)
535
+ const keys = extractRequirementKeys(tierConfigs);
536
+ // ['stays_partner', 'stays_member_origin', 'points', ...]
537
+ ```
538
+
539
+ **Available Labels:**
540
+ - `stays_member_origin` → "Origin stays"
541
+ - `stays_network` → "Network stays"
542
+ - `stays_partner` → "[Partner] stays"
543
+ - `nights_member_origin` → "Origin nights"
544
+ - `nights_network` → "Network nights"
545
+ - `nights_partner` → "[Partner] nights"
546
+ - `points` → "AKIN points"
547
+ - `network_points` → "Network points"
548
+ - `spend` → "Total spend"
549
+ - `referrals` → "Referrals"
550
+ - `achievement_tattoo` → "[Partner] tattoo"
551
+ - `achievement_invitation` → "Invitation"
552
+ - `country_completion_mx` → "Mexico complete"
553
+ - `country_completion_pe` → "Peru complete"
554
+ - `country_completion_co` → "Colombia complete"
555
+
556
+ ## Types
557
+
558
+ ### AkinMember
559
+
560
+ ```typescript
561
+ interface AkinMember {
562
+ id: string;
563
+ email: string;
564
+ firstName: string;
565
+ lastName: string;
566
+ phone?: string;
567
+ points: number;
568
+ currentTier: 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4';
569
+ status: 'ACTIVE' | 'SUSPENDED' | 'DELETED';
570
+ loyaltyNumber: string;
571
+ vibePreference?: string;
572
+ perkPreferences?: string[];
573
+ createdAt: Date;
574
+ updatedAt: Date;
575
+ }
576
+ ```
577
+
578
+ ### TierConfig
579
+
580
+ ```typescript
581
+ interface TierConfig {
582
+ id: string;
583
+ tierName: string; // 'TIER1' | 'TIER2' | 'TIER3' | 'TIER4'
584
+ displayName: string; // 'Explorer' | 'Voyager' | etc.
585
+ displayOrder: number;
586
+ maxPerksAllowed: number;
587
+ tierColor?: string;
588
+ tierIcon?: string;
589
+ tierByline?: string;
590
+ eligibility?: string; // Note: eligibility (not eligibilityText)
591
+ requirements?: TierRequirement[]; // Key/value tier requirements
592
+ }
593
+ ```
594
+
595
+ ### TierRequirement
596
+
597
+ ```typescript
598
+ interface TierRequirement {
599
+ id: string;
600
+ tierConfigId: string;
601
+ requirementKey: string; // e.g., 'stays_member_origin', 'points'
602
+ requirementValue: string; // Threshold value
603
+ valueType: 'INT' | 'FLOAT' | 'DATE' | 'STRING';
604
+ operator: 'eq' | 'gt' | 'gte' | 'lt' | 'lte';
605
+ logicGroup: number; // For AND/OR logic combinations
606
+ displayOrder: number;
607
+ displayName?: string; // Custom display override
608
+ active: boolean;
609
+ }
610
+ ```
611
+
612
+ ### LoyaltyTransaction
613
+
614
+ ```typescript
615
+ interface LoyaltyTransaction {
616
+ id: string;
617
+ memberId: string;
618
+ bookingId?: string;
619
+ pointsChange: number;
620
+ transactionType: 'EARNED' | 'SPENT' | 'EXPIRED' | 'ADJUSTED';
621
+ reason?: string;
622
+ description?: string;
623
+ balanceAfter: number;
624
+ createdAt: Date;
625
+ }
626
+ ```
627
+
628
+ ### TierProgress
629
+
630
+ ```typescript
631
+ interface TierProgress {
632
+ currentTier: string;
633
+ currentTierName: string;
634
+ nextTier: string | null;
635
+ nextTierName: string | null;
636
+ percentage: number;
637
+ pointsNeeded: number;
638
+ totalStays: number;
639
+ originStays: number; // Note: originStays (not originPartnerStays)
640
+ }
641
+ ```
642
+
643
+ ## Environment Variables
644
+
645
+ ```bash
646
+ # Required - Partner credentials
647
+ NEXT_PUBLIC_AKIN_PARTNER_ID=your-partner-id
648
+ NEXT_PUBLIC_AKIN_API_KEY=your-api-key
649
+
650
+ # Required - SDK infrastructure (provided by Akin during onboarding)
651
+ NEXT_PUBLIC_AKIN_SDK_API_URL=https://api.akintravel.com/graphql
652
+ NEXT_PUBLIC_AKIN_SDK_GIP_TENANT_ID=your-gip-tenant-id
653
+ NEXT_PUBLIC_AKIN_SDK_FIREBASE_API_KEY=your-firebase-api-key
654
+ NEXT_PUBLIC_AKIN_SDK_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
655
+ NEXT_PUBLIC_AKIN_SDK_FIREBASE_PROJECT_ID=your-firebase-project-id
656
+ NEXT_PUBLIC_AKIN_SDK_FIREBASE_APP_ID=your-firebase-app-id
657
+
658
+ # Optional - Environment-specific API URLs
659
+ NEXT_PUBLIC_AKIN_SDK_STAGING_API_URL=https://staging-api.example.com/graphql
660
+ NEXT_PUBLIC_AKIN_SDK_DEV_API_URL=http://localhost:4000/graphql
661
+ ```
662
+
663
+ ## Security Best Practices
664
+
665
+ ### API Key Handling
666
+
667
+ **IMPORTANT:** Always use environment variables for your API key. Never commit API keys to source control.
668
+
669
+ ```tsx
670
+ // ✅ CORRECT - Use environment variable
671
+ apiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY!
672
+
673
+ // ❌ WRONG - Never hardcode API keys
674
+ apiKey: 'pk_live_abc123...'
675
+
676
+ // ❌ WRONG - Never use fallback values in production
677
+ apiKey: process.env.NEXT_PUBLIC_AKIN_API_KEY || 'demo-key'
678
+ ```
679
+
680
+ ### Debug Mode
681
+
682
+ Only enable debug mode in development:
683
+
684
+ ```tsx
685
+ debug: process.env.NODE_ENV === 'development'
686
+ ```
687
+
688
+ ### Environment File (.env.local)
689
+
690
+ 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.
691
+
692
+ ```bash
693
+ # Add to .gitignore
694
+ .env.local
695
+ .env*.local
696
+ ```
697
+
698
+ ### CORS
699
+
700
+ The SDK automatically handles CORS with the Akin API. If you need to allow additional origins, contact your Akin partner representative.
701
+
702
+ ## CSS Styling Notes
703
+
704
+ ### Link Color Override
705
+
706
+ If you have global link styles that override button text colors, use a more specific selector:
707
+
708
+ ```css
709
+ /* ❌ This will override button text colors */
710
+ a {
711
+ @apply text-blue-600;
712
+ }
713
+
714
+ /* ✅ Use this instead - excludes elements with explicit text colors */
715
+ a:not([class*="text-"]) {
716
+ @apply text-blue-600;
717
+ }
718
+ ```
719
+
720
+ ### Phone Input Styles
721
+
722
+ Import the phone input CSS:
723
+
724
+ ```tsx
725
+ import 'react-phone-number-input/style.css';
726
+ ```
727
+
728
+ ## Examples
729
+
730
+ ### Magic Link Verification Page
731
+
732
+ Create an `/auth/verify` route to handle magic link callbacks:
733
+
734
+ ```tsx
735
+ 'use client';
736
+
737
+ import { Suspense, useEffect, useState } from 'react';
738
+ import { useRouter, useSearchParams } from 'next/navigation';
739
+ import { useAkinAuth } from '@akin-online/partner-sdk';
740
+
741
+ function VerifyContent() {
742
+ const router = useRouter();
743
+ const searchParams = useSearchParams();
744
+ const { verifyMagicLink } = useAkinAuth();
745
+ const [state, setState] = useState<'loading' | 'success' | 'error'>('loading');
746
+ const [error, setError] = useState<string | null>(null);
747
+
748
+ useEffect(() => {
749
+ const token = searchParams.get('token');
750
+
751
+ if (!token) {
752
+ setError('Invalid verification link');
753
+ setState('error');
754
+ return;
755
+ }
756
+
757
+ const verify = async () => {
758
+ const result = await verifyMagicLink(token);
759
+
760
+ if (!result.success) {
761
+ setError(result.error || 'Verification failed');
762
+ setState('error');
763
+ return;
764
+ }
765
+
766
+ setState('success');
767
+ setTimeout(() => router.push('/rewards'), 1500);
768
+ };
769
+
770
+ verify();
771
+ }, [searchParams, verifyMagicLink, router]);
772
+
773
+ if (state === 'loading') return <p>Verifying...</p>;
774
+ if (state === 'success') return <p>Verified! Redirecting...</p>;
775
+ return <p>Error: {error}</p>;
776
+ }
777
+
778
+ export default function VerifyPage() {
779
+ return (
780
+ <Suspense fallback={<p>Loading...</p>}>
781
+ <VerifyContent />
782
+ </Suspense>
783
+ );
784
+ }
785
+ ```
786
+
787
+ ### Complete Passwordless Signup
788
+
789
+ ```tsx
790
+ 'use client';
791
+
792
+ import { useState } from 'react';
793
+ import { SignupForm, PhoneInput } from '@akin-online/partner-sdk';
794
+ import 'react-phone-number-input/style.css';
795
+
796
+ export default function SignupPage() {
797
+ const [signupComplete, setSignupComplete] = useState(false);
798
+ const [submittedEmail, setSubmittedEmail] = useState('');
799
+
800
+ if (signupComplete) {
801
+ return (
802
+ <div className="text-center">
803
+ <h1>Check your email!</h1>
804
+ <p>We sent a verification link to {submittedEmail}</p>
805
+ </div>
806
+ );
807
+ }
808
+
809
+ return (
810
+ <SignupForm
811
+ passwordless
812
+ onSuccess={() => {}}
813
+ onError={(error) => console.error(error)}
814
+ >
815
+ {({
816
+ firstName, setFirstName,
817
+ lastName, setLastName,
818
+ email, setEmail,
819
+ phone, setPhone,
820
+ termsAccepted, setTermsAccepted,
821
+ marketingOptIn, setMarketingOptIn,
822
+ isLoading, error,
823
+ handleSubmit,
824
+ clearError,
825
+ }) => (
826
+ <form
827
+ onSubmit={(e) => {
828
+ e.preventDefault();
829
+ setSubmittedEmail(email);
830
+ handleSubmit(e).then(() => {
831
+ if (!error) setSignupComplete(true);
832
+ });
833
+ }}
834
+ >
835
+ <div className="grid grid-cols-2 gap-4">
836
+ <input
837
+ value={firstName}
838
+ onChange={(e) => { setFirstName(e.target.value); clearError(); }}
839
+ placeholder="First name"
840
+ required
841
+ />
842
+ <input
843
+ value={lastName}
844
+ onChange={(e) => { setLastName(e.target.value); clearError(); }}
845
+ placeholder="Last name"
846
+ required
847
+ />
848
+ </div>
849
+
850
+ <input
851
+ type="email"
852
+ value={email}
853
+ onChange={(e) => { setEmail(e.target.value); clearError(); }}
854
+ placeholder="Email"
855
+ required
856
+ />
857
+
858
+ <PhoneInput
859
+ value={phone}
860
+ onChange={(value) => { setPhone(value); clearError(); }}
861
+ defaultCountry="US"
862
+ placeholder="Phone number"
863
+ required
864
+ />
865
+
866
+ <label>
867
+ <input
868
+ type="checkbox"
869
+ checked={termsAccepted}
870
+ onChange={(e) => setTermsAccepted(e.target.checked)}
871
+ required
872
+ />
873
+ I agree to the Terms of Service and Privacy Policy
874
+ </label>
875
+
876
+ <label>
877
+ <input
878
+ type="checkbox"
879
+ checked={marketingOptIn}
880
+ onChange={(e) => setMarketingOptIn(e.target.checked)}
881
+ />
882
+ Send me updates and offers
883
+ </label>
884
+
885
+ {error && <p className="error">{error}</p>}
886
+
887
+ <button type="submit" disabled={isLoading}>
888
+ {isLoading ? 'Creating account...' : 'Create Account'}
889
+ </button>
890
+ </form>
891
+ )}
892
+ </SignupForm>
893
+ );
894
+ }
895
+ ```
896
+
897
+ ### Complete Login Page with Passkeys
898
+
899
+ ```tsx
900
+ 'use client';
901
+
902
+ import { useAkinAuth, LoginForm } from '@akin-online/partner-sdk';
903
+ import { useRouter } from 'next/navigation';
904
+ import { useEffect } from 'react';
905
+
906
+ export default function LoginPage() {
907
+ const router = useRouter();
908
+ const { isAuthenticated } = useAkinAuth();
909
+
910
+ useEffect(() => {
911
+ if (isAuthenticated) router.push('/rewards');
912
+ }, [isAuthenticated, router]);
913
+
914
+ return (
915
+ <div className="login-container">
916
+ <h1>Welcome Back</h1>
917
+
918
+ <LoginForm
919
+ onSuccess={() => router.push('/rewards')}
920
+ onError={(error) => console.error(error)}
921
+ >
922
+ {({
923
+ mode, setMode,
924
+ email, setEmail,
925
+ isLoading, error,
926
+ passkeySupported,
927
+ handlePasskeyLogin,
928
+ handleMagicLink,
929
+ handleResendMagicLink,
930
+ clearError,
931
+ }) => (
932
+ <div>
933
+ {error && <p className="error">{error}</p>}
934
+
935
+ {mode === 'passkey' && (
936
+ <div>
937
+ <button
938
+ onClick={handlePasskeyLogin}
939
+ disabled={!passkeySupported || isLoading}
940
+ >
941
+ {isLoading ? 'Signing in...' : 'Sign in with Passkey'}
942
+ </button>
943
+ <button onClick={() => { clearError(); setMode('magic-link'); }}>
944
+ Sign in with Email
945
+ </button>
946
+ </div>
947
+ )}
948
+
949
+ {mode === 'magic-link' && (
950
+ <div>
951
+ <input
952
+ type="email"
953
+ value={email}
954
+ onChange={(e) => { setEmail(e.target.value); clearError(); }}
955
+ placeholder="you@example.com"
956
+ />
957
+ <button
958
+ onClick={handleMagicLink}
959
+ disabled={isLoading || !email}
960
+ >
961
+ {isLoading ? 'Sending...' : 'Send sign-in link'}
962
+ </button>
963
+ {passkeySupported && (
964
+ <button onClick={() => { clearError(); setMode('passkey'); }}>
965
+ ← Back to passkey sign-in
966
+ </button>
967
+ )}
968
+ </div>
969
+ )}
970
+
971
+ {mode === 'magic-link-sent' && (
972
+ <div className="text-center">
973
+ <h2>Check your email</h2>
974
+ <p>We sent a sign-in link to {email}</p>
975
+ <button onClick={handleResendMagicLink} disabled={isLoading}>
976
+ {isLoading ? 'Resending...' : 'Resend link'}
977
+ </button>
978
+ <button onClick={() => { clearError(); setMode('magic-link'); }}>
979
+ Try a different email
980
+ </button>
981
+ </div>
982
+ )}
983
+ </div>
984
+ )}
985
+ </LoginForm>
986
+ </div>
987
+ );
988
+ }
989
+ ```
990
+
991
+ ### Complete Rewards Dashboard
992
+
993
+ ```tsx
994
+ 'use client';
995
+
996
+ import {
997
+ useAkinAuth,
998
+ useAkinLoyalty,
999
+ TierCard,
1000
+ TierProgress,
1001
+ TransactionList,
1002
+ PointsDisplay,
1003
+ } from '@akin-online/partner-sdk';
1004
+ import { useRouter } from 'next/navigation';
1005
+ import { useEffect } from 'react';
1006
+
1007
+ export default function RewardsPage() {
1008
+ const router = useRouter();
1009
+ const { member, isAuthenticated, isLoading: authLoading, signOut } = useAkinAuth();
1010
+ const { isLoading: loyaltyLoading } = useAkinLoyalty();
1011
+
1012
+ useEffect(() => {
1013
+ if (!authLoading && !isAuthenticated) {
1014
+ router.push('/login');
1015
+ }
1016
+ }, [isAuthenticated, authLoading, router]);
1017
+
1018
+ if (authLoading || loyaltyLoading) {
1019
+ return <p>Loading...</p>;
1020
+ }
1021
+
1022
+ if (!isAuthenticated) return null;
1023
+
1024
+ return (
1025
+ <div className="rewards-container">
1026
+ <header>
1027
+ <h1>Welcome back, {member?.firstName}!</h1>
1028
+ <p>Loyalty #: {member?.loyaltyNumber}</p>
1029
+ <button onClick={() => signOut().then(() => router.push('/'))}>
1030
+ Sign Out
1031
+ </button>
1032
+ </header>
1033
+
1034
+ <div className="grid grid-cols-2 gap-4">
1035
+ <div className="card">
1036
+ <h2>Your Points</h2>
1037
+ <PointsDisplay>
1038
+ {({ formattedPoints }) => (
1039
+ <p className="text-4xl font-bold">{formattedPoints}</p>
1040
+ )}
1041
+ </PointsDisplay>
1042
+ </div>
1043
+
1044
+ <div className="card">
1045
+ <h2>Current Tier</h2>
1046
+ <TierCard>
1047
+ {({ displayName, tier }) => (
1048
+ <div>
1049
+ <p className="text-2xl font-bold">{displayName}</p>
1050
+ <p className="text-sm">Tier {tier.replace('TIER', '')}</p>
1051
+ </div>
1052
+ )}
1053
+ </TierCard>
1054
+ </div>
1055
+ </div>
1056
+
1057
+ <div className="card">
1058
+ <h2>Progress to Next Tier</h2>
1059
+ <TierProgress>
1060
+ {({ currentTierName, nextTierName, percentage, pointsNeeded }) => (
1061
+ nextTierName ? (
1062
+ <div>
1063
+ <div className="flex justify-between">
1064
+ <span>{currentTierName}</span>
1065
+ <span>{nextTierName}</span>
1066
+ </div>
1067
+ <div className="progress-bar">
1068
+ <div style={{ width: `${percentage}%` }} />
1069
+ </div>
1070
+ <p>{pointsNeeded.toLocaleString()} points to {nextTierName}</p>
1071
+ </div>
1072
+ ) : (
1073
+ <p>You've reached the highest tier!</p>
1074
+ )
1075
+ )}
1076
+ </TierProgress>
1077
+ </div>
1078
+
1079
+ <div className="card">
1080
+ <h2>Recent Activity</h2>
1081
+ <TransactionList limit={10}>
1082
+ {({ transactions, isLoading, hasMore, loadMore }) => (
1083
+ <div>
1084
+ {transactions.length === 0 ? (
1085
+ <p>No transactions yet</p>
1086
+ ) : (
1087
+ <>
1088
+ <ul>
1089
+ {transactions.map((tx) => (
1090
+ <li key={tx.id} className="flex justify-between py-3">
1091
+ <div>
1092
+ <p>{tx.reason || tx.description || 'Transaction'}</p>
1093
+ <p className="text-sm text-gray-500">
1094
+ {new Date(tx.createdAt).toLocaleDateString()}
1095
+ </p>
1096
+ </div>
1097
+ <span className={tx.pointsChange > 0 ? 'text-green-600' : 'text-red-600'}>
1098
+ {tx.pointsChange > 0 ? '+' : ''}{tx.pointsChange.toLocaleString()}
1099
+ </span>
1100
+ </li>
1101
+ ))}
1102
+ </ul>
1103
+ {hasMore && (
1104
+ <button onClick={loadMore} disabled={isLoading}>
1105
+ {isLoading ? 'Loading...' : 'Load More'}
1106
+ </button>
1107
+ )}
1108
+ </>
1109
+ )}
1110
+ </div>
1111
+ )}
1112
+ </TransactionList>
1113
+ </div>
1114
+ </div>
1115
+ );
1116
+ }
1117
+ ```
1118
+
1119
+ ## Development / Examples
1120
+
1121
+ ### Viajero Demo
1122
+
1123
+ A complete example implementation is available in the monorepo at `docs/partner-sdk-examples/viajero/`. This demonstrates a partner rewards site using the SDK with:
1124
+
1125
+ - Login page with passkey and magic link authentication
1126
+ - Signup form with passwordless registration
1127
+ - Rewards dashboard with tier display and points
1128
+ - Full Tailwind CSS styling
1129
+
1130
+ **Running the demo:**
1131
+
1132
+ ```bash
1133
+ # From monorepo root
1134
+ cd docs/partner-sdk-examples/viajero
1135
+ npm install
1136
+ npm run dev
1137
+ ```
1138
+
1139
+ The demo runs at **http://localhost:3003**
1140
+
1141
+ **Demo structure:**
1142
+ ```
1143
+ viajero/
1144
+ ├── app/
1145
+ │ ├── layout.tsx # AkinProvider setup
1146
+ │ ├── page.tsx # Landing page
1147
+ │ ├── login/ # Login with passkey/magic link
1148
+ │ ├── signup/ # Passwordless signup
1149
+ │ ├── rewards/ # Authenticated rewards dashboard
1150
+ │ └── auth/verify/ # Magic link verification
1151
+ ├── components/ # Reusable UI components
1152
+ └── .env.local.example # Environment variables template
1153
+ ```
1154
+
1155
+ ### Local SDK Development
1156
+
1157
+ When developing the SDK itself, the demo uses a file reference to the local package:
1158
+
1159
+ ```json
1160
+ {
1161
+ "dependencies": {
1162
+ "@akin-online/partner-sdk": "file:../../../packages/partner-sdk"
1163
+ }
1164
+ }
1165
+ ```
1166
+
1167
+ After making changes to the SDK, rebuild and restart the demo:
1168
+
1169
+ ```bash
1170
+ # From monorepo root
1171
+ npm run build -w @akin-online/partner-sdk
1172
+ cd docs/partner-sdk-examples/viajero && npm run dev
1173
+ ```
1174
+
1175
+ ## Troubleshooting
1176
+
1177
+ ### "Loading..." stuck after passkey authentication
1178
+
1179
+ Ensure you're using the latest SDK version. The `signInWithPasskey` function properly resets loading state in the `finally` block.
1180
+
1181
+ ### Button text color not showing
1182
+
1183
+ Check for global CSS rules that override text colors. See [CSS Styling Notes](#css-styling-notes).
1184
+
1185
+ ### Phone input not styled
1186
+
1187
+ Import the phone input CSS: `import 'react-phone-number-input/style.css';`
1188
+
1189
+ ### Magic link not working
1190
+
1191
+ 1. Verify your `NEXT_PUBLIC_AKIN_API_KEY` is set correctly
1192
+ 2. Check that your domain is in the allowed origins for your API key
1193
+ 3. Ensure you have an `/auth/verify` route to handle the callback
1194
+
1195
+ ### GraphQL errors
1196
+
1197
+ If you see errors about field types or non-existent fields, ensure you're using the latest SDK version. Common issues:
1198
+ - Use `String!` (not `ID!`) for member/partner IDs in queries
1199
+ - Use `originStays` (not `originPartnerStays`)
1200
+ - Use `eligibility` (not `eligibilityText`)
1201
+
1202
+ ## Support
1203
+
1204
+ For questions or issues, contact your Akin partner representative or email support@akintravel.com