hakgyo-expo-sdk
v1.0.48
Published
SDK for integrating hakgyo webapp authentication and API in Expo apps
Maintainers
Readme
hakgyo-expo-sdk
TypeScript SDK for integrating hakgyo webapp authentication and API in Expo/React Native applications.
Features
- Better-Auth Integration - Seamless authentication with Expo plugin support
- Secure Storage - Uses
expo-secure-storefor secure session management - Auto Session Refresh - Automatic token refresh before expiry
- Retry Logic - Built-in retry mechanism with exponential backoff
- Full TypeScript Support - Complete type definitions for all API responses
- React Hooks - Authentication hooks for easy state management
- Configurable - Customize timeouts, retries, logging, and more
- Structured Error Handling - Dedicated error classes for different scenarios
- Pagination Support - Standardized query parameters for paginated responses
Installation
bun add hakgyo-expo-sdk
# or
npm install hakgyo-expo-sdk
# or
yarn add hakgyo-expo-sdkPeer Dependencies
This SDK requires the following peer dependencies:
expo>=50.0.0
react>=18.0.0
react-native>=0.73.0
expo-secure-store>=12.0.0
@react-native-google-signin/google-signin>=16.0.0Quick Start
1. Initialize the SDK
Initialize the SDK in your app entry point (e.g., App.tsx or app/_layout.tsx):
import { initSDK } from 'hakgyo-expo-sdk';
initSDK({
baseURL: 'https://your-hakgyo-api.com',
// Optional: override default config
auth: {
storagePrefix: 'hakgyo_auth',
sessionRefreshThreshold: 5, // minutes
autoRefresh: true,
deepLinkScheme: 'hakgyo://',
},
api: {
timeout: 30000, // ms
retries: 3,
retryDelay: 1000, // ms
},
logging: {
enabled: false,
level: 'error', // 'debug' | 'info' | 'warn' | 'error'
},
platform: {
deviceId: 'your-device-id',
platformType: 'ios', // 'ios' | 'android' | 'web'
},
});2. Wrap Your App with AuthProvider
import { AuthProvider } from 'hakgyo-expo-sdk';
export default function App() {
return (
<AuthProvider>
<YourApp />
</AuthProvider>
);
}3. Use Authentication
import { useAuth } from 'hakgyo-expo-sdk';
function LoginScreen() {
const { signIn, signUp, signOut, isLoading, user } = useAuth();
const handleLogin = async () => {
try {
await signIn('[email protected]', 'password');
} catch (error) {
console.error('Login failed:', error);
}
};
const handleSignUp = async () => {
try {
await signUp('[email protected]', 'password', 'John Doe');
} catch (error) {
console.error('Sign up failed:', error);
}
};
return (
// Your login UI
);
}4. Make API Calls
import { kelasApi, materiApi, vocabularyApi } from 'hakgyo-expo-sdk';
// Fetch kelas list
const kelasList = await kelasApi.list();
// Get specific materi
const materi = await materiApi.get('materi-id');
// Get daily vocabulary
const dailyVocab = await vocabularyApi.getDaily();Configuration
HakgyoSDKConfig
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| baseURL | string | Yes | - | Base URL of the Hakgyo API |
| auth.storagePrefix | string | No | 'hakgyo_auth' | Prefix for secure storage keys |
| auth.sessionRefreshThreshold | number | No | 5 | Minutes before expiry to refresh session |
| auth.autoRefresh | boolean | No | true | Enable automatic session refresh |
| auth.deepLinkScheme | string | No | 'hakgyo://' | Deep link scheme for auth callbacks |
| api.timeout | number | No | 30000 | Request timeout in milliseconds |
| api.retries | number | No | 3 | Number of retry attempts for failed requests |
| api.retryDelay | number | No | 1000 | Initial delay between retries in milliseconds |
| logging.enabled | boolean | No | false | Enable debug logging |
| logging.level | 'debug'\|'info'\|'warn'\|'error' | No | 'error' | Minimum log level to output |
| platform.deviceId | string | No | - | Unique device identifier |
| platform.platformType | 'ios'\|'android'\|'web' | No | - | Platform type |
Getting Current Config
import { getConfig } from 'hakgyo-expo-sdk';
const currentConfig = getConfig();Authentication
Auth Functions
Sign In with Email
import { signInWithEmail } from 'hakgyo-expo-sdk';
const session = await signInWithEmail('[email protected]', 'password');Sign Up with Email
import { signUpWithEmail } from 'hakgyo-expo-sdk';
const session = await signUpWithEmail('[email protected]', 'password', 'John Doe');Sign In with Google
New in v1.0.33: The SDK now accepts raw Google Sign-In data directly without manual transformation.
import { signInWithGoogle } from 'hakgyo-expo-sdk';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
// Sign in with Google - no transformation needed!
const userInfo = await GoogleSignin.signIn();
const session = await signInWithGoogle(userInfo.data);The signInWithGoogle function accepts raw data from @react-native-google-signin/google-signin:
idToken: The ID token from Google OAuth (required)serverAuthCode: Server auth code (optional, can benull)scopes: OAuth scopes granted (optional)user: User information from Google (with nullable fields)
Backward Compatibility: The function also supports the legacy transformed format for existing code.
Legacy Format (deprecated but still supported):
// This still works but is no longer recommended
const googleSignInData = {
idToken: '...',
user: { email: '...', name: '...', id: '...' }
};
const session = await signInWithGoogle(googleSignInData);Important: Use v1.0.33 or later for the raw data API. v1.0.32 had a bug that caused "invalid token" errors when using raw data.
Sign Out
import { signOut } from 'hakgyo-expo-sdk';
await signOut();React Hooks
useAuth
Access authentication state and methods:
import { useAuth } from 'hakgyo-expo-sdk';
function MyComponent() {
const {
user, // Current user object
session, // Current session object
loading, // Loading state
error, // Error object if session fetch failed (e.g., network error)
signInWithEmail, // Sign in with email function
signUpWithEmail, // Sign up with email function
signInWithGoogle, // Sign in with Google function
signInWithSocial, // Sign in with social provider function
signOut, // Sign out function
refreshSession, // Refresh session function
clearError, // Clear error state function
} = useAuth();
if (loading) return <LoadingSpinner />;
// Handle network errors gracefully
if (error) {
const isNetworkError = error.message.includes('Network') ||
error.message.includes('Failed to fetch');
if (isNetworkError) {
return (
<View>
<Text>Unable to connect to server. Please check your connection.</Text>
<Button onPress={clearError} title="Retry" />
</View>
);
}
}
if (!session) return <LoginScreen />;
return <Dashboard user={user} />;
}Error Handling
The error property is set when the session fetch fails (e.g., due to network issues). This allows you to:
- Distinguish between "no session" and "failed to fetch session"
- Show user-friendly error messages
- Implement retry logic
import { useAuth } from 'hakgyo-expo-sdk';
import { Alert } from 'react-native';
function App() {
const { error, clearError, loading } = useAuth();
useEffect(() => {
if (error && !loading) {
Alert.alert(
'Connection Error',
'Unable to connect to the server. Please check your connection and try again.',
[{ text: 'OK', onPress: clearError }]
);
}
}, [error, loading]);
// ... rest of your component
}useSession
Convenience hook for session/user data:
import { useSession } from 'hakgyo-expo-sdk';
function UserProfile() {
const { user, session, loading, error, clearError } = useSession();
if (loading) return <LoadingSpinner />;
if (error) {
return (
<View>
<Text>Error loading session: {error.message}</Text>
<Button onPress={clearError} title="Clear Error" />
</View>
);
}
return (
<View>
<Text>Welcome, {user?.name}</Text>
</View>
);
}Session Management
import { SessionManager } from 'hakgyo-expo-sdk';
// Store session
await SessionManager.setSession(session);
// Get current session (auto-checks expiry and refreshes if needed)
const session = await SessionManager.getSession();
// Clear session
await SessionManager.clearSession();
// Manually refresh session
const refreshedSession = await SessionManager.refreshSession();API Usage
ApiClient
The SDK provides a generic HTTP client with built-in authentication:
import { ApiClient } from 'hakgyo-expo-sdk';
const client = new ApiClient();
// GET request
const data = await client.get<T>('/path', { param1: 'value' });
// POST request
const result = await client.post<T>('/path', { key: 'value' });
// PUT request
const updated = await client.put<T>('/path', { key: 'value' });
// PATCH request
const patched = await client.patch<T>('/path', { key: 'value' });
// DELETE request
await client.delete('/path');
// Generic request
const response = await client.request<T>(
'GET',
'/path',
{ body: {} },
{ 'X-Custom-Header': 'value' }
);Kelas API
import { kelasApi } from 'hakgyo-expo-sdk';
// List all kelas
const kelasList = await kelasApi.list();
// Get specific kelas
const kelas = await kelasApi.get('kelas-id');
// Create new kelas
const newKelas = await kelasApi.create({
title: 'Korean 101',
description: 'Beginner Korean course',
type: 'COURSE',
level: 'BEGINNER',
pricing: { amount: 0, currency: 'IDR' },
});
// Update kelas
const updated = await kelasApi.update('kelas-id', { title: 'Updated Title' });
// Delete kelas
await kelasApi.delete('kelas-id');
// Join a kelas
await kelasApi.join('kelas-id');
// Leave a kelas
await kelasApi.leave('kelas-id');
// Get materials for a kelas
const materials = await kelasApi.getMaterials('kelas-id');Materi API
import { materiApi } from 'hakgyo-expo-sdk';
// Get materi
const materi = await materiApi.get('materi-id');
// Mark materi as complete
await materiApi.complete('materi-id');
// Submit assessment
const result = await materiApi.submitAssessment('materi-id', {
answers: [{ questionId: 'q1', answer: 'A' }],
});
// Get assessment config
const config = await materiApi.getAssessmentConfig('materi-id');Vocabulary API
import { vocabularyApi } from 'hakgyo-expo-sdk';
// Get daily vocabulary
const daily = await vocabularyApi.getDaily();
// Get vocabulary set
const set = await vocabularyApi.getSet('set-id');
// List vocabulary sets
const sets = await vocabularyApi.listSets();
// Create vocabulary set
const newSet = await vocabularyApi.createSet({
title: 'Basic Greetings',
icon: '👋',
isPublic: true,
});
// List items in a set
const items = await vocabularyApi.listItems('set-id');
// Get specific item
const item = await vocabularyApi.getItem('item-id');
// Add item to set
const newItem = await vocabularyApi.addItem('set-id', {
korean: '안녕하세요',
indonesian: 'Halo',
type: 'WORD',
pos: 'NOUN',
audioUrl: 'https://...',
});
// Update item
const updated = await vocabularyApi.updateItem('item-id', {
indonesian: 'Halo semuanya',
});
// Delete item
await vocabularyApi.deleteItem('item-id');
// Mark item as learned
await vocabularyApi.markLearned('item-id');Soal API
import { soalApi } from 'hakgyo-expo-sdk';
// Get question collection
const collection = await soalApi.getCollection('collection-id');
// List collections
const collections = await soalApi.listCollections();
// Create collection
const newCollection = await soalApi.createCollection({
title: 'Grammar Practice',
description: 'Basic grammar exercises',
difficulty: 'EASY',
});
// Start practice session
const session = await soalApi.practice('collection-id');
// Submit practice answers
const result = await soalApi.submitPractice('session-id', {
answers: [{ questionId: 'q1', answer: 'A' }],
});Tryout API
import { tryoutApi } from 'hakgyo-expo-sdk';
// Get tryout
const tryout = await tryoutApi.get('tryout-id');
// List active tryouts
const active = await tryoutApi.listActive();
// Participate in tryout
const participation = await tryoutApi.participate('tryout-id');
// Submit tryout answers
const result = await tryoutApi.submit('tryout-id', {
answers: [{ questionId: 'q1', answer: 'A' }],
});
// Get tryout results
const results = await tryoutApi.getResults('tryout-id');Posts API
import { postsApi } from 'hakgyo-expo-sdk';
// List posts
const posts = await postsApi.list();
// Get post
const post = await postsApi.get('post-id');
// Create post
const newPost = await postsApi.create({
title: 'My First Post',
content: 'Hello everyone!',
});
// Update post
const updated = await postsApi.update('post-id', { title: 'Updated' });
// Delete post
await postsApi.delete('post-id');
// Like post
await postsApi.like('post-id');
// Unlike post
await postsApi.unlike('post-id');
// Get comments
const comments = await postsApi.getComments('post-id');
// Add comment
const comment = await postsApi.addComment('post-id', {
content: 'Great post!',
});User API
import { userApi } from 'hakgyo-expo-sdk';
// Get profile
const profile = await userApi.getProfile();
// Update profile
const updated = await userApi.updateProfile({
name: 'John Doe',
bio: 'Learning Korean!',
});
// Get user's classes
const classes = await userApi.getClasses();
// Get tryout results
const results = await userApi.getTryoutResults();
// Register push token
await userApi.registerPushToken('push-token');Notifications API
import { notificationsApi } from 'hakgyo-expo-sdk';
// Register push notification token
await notificationsApi.registerToken('push-token');
// Unregister push notification token
await notificationsApi.unregisterToken('push-token');Type Definitions
Auth Types
// User roles
type UserRole = 'MURID' | 'GURU' | 'ADMIN';
// User tiers
type UserTier = 'FREE' | 'PREMIUM' | 'CUSTOM';
// User object
interface User {
id: string;
email: string;
name: string;
role: UserRole;
tier: UserTier;
createdAt: Date;
updatedAt: Date;
}
// Session object
interface Session {
user: User;
token: string;
expiresAt: Date;
refreshToken?: string;
}
// Sign in response
interface SignInResponse {
user: User;
session: Session;
}
// Sign up response
interface SignUpResponse {
user: User;
session: Session;
}
// Sign out response
interface SignOutResponse {
success: boolean;
}Model Types
Kelas
interface Kelas {
id: string;
title: string;
description?: string;
type: 'COURSE' | 'WORKSHOP' | 'WEBINAR';
level: 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED';
pricing: {
amount: number;
currency: string;
};
author: {
id: string;
name: string;
};
createdAt: Date;
updatedAt: Date;
}Materi
interface Materi {
id: string;
kelasId: string;
title: string;
content: string;
order: number;
isDemo: boolean;
assessmentConfig?: {
enabled: boolean;
passScore: number;
timeLimit?: number;
};
createdAt: Date;
updatedAt: Date;
}Vocabulary
interface VocabularySet {
id: string;
title: string;
icon?: string;
isPublic: boolean;
itemCount: number;
createdAt: Date;
updatedAt: Date;
}
interface VocabularyItem {
id: string;
setId: string;
korean: string;
indonesian: string;
type: 'WORD' | 'SENTENCE';
pos?: string; // Part of speech
audioUrl?: string;
isLearned: boolean;
createdAt: Date;
updatedAt: Date;
}Soal
interface Soal {
id: string;
question: string;
options: Opsi[];
correctAnswer: string;
explanation?: string;
difficulty: 'EASY' | 'MEDIUM' | 'HARD';
}
interface Opsi {
id: string;
label: string;
value: string;
}
interface KoleksiSoal {
id: string;
title: string;
description?: string;
difficulty: 'EASY' | 'MEDIUM' | 'HARD';
questionCount: number;
createdAt: Date;
}
interface PracticeSession {
id: string;
collectionId: string;
userId: string;
startedAt: Date;
completedAt?: Date;
}
interface PracticeResult {
sessionId: string;
score: number;
correctCount: number;
totalQuestions: number;
answers: Array<{
questionId: string;
answer: string;
isCorrect: boolean;
}>;
}Tryout
interface Tryout {
id: string;
title: string;
description?: string;
startTime: Date;
endTime: Date;
duration: number; // minutes
questionCount: number;
isActive: boolean;
createdAt: Date;
}
interface TryoutParticipant {
id: string;
tryoutId: string;
userId: string;
joinedAt: Date;
submittedAt?: Date;
}
interface TryoutResult {
id: string;
participantId: string;
score: number;
rank?: number;
answers: Array<{
questionId: string;
answer: string;
isCorrect: boolean;
}>;
}Posts
interface Post {
id: string;
authorId: string;
title: string;
content: string;
likesCount: number;
commentsCount: number;
createdAt: Date;
updatedAt: Date;
}
interface Comment {
id: string;
postId: string;
authorId: string;
content: string;
createdAt: Date;
updatedAt: Date;
}Error Handling
The SDK provides structured error classes for different error scenarios:
Error Classes
// Base error class
class HakgyoError extends Error {
code: string;
constructor(message: string, code: string);
}
// Authentication errors
class AuthError extends HakgyoError {
constructor(message: string, code: string);
}
// API errors with status code and response data
class ApiError extends HakgyoError {
statusCode: number;
response?: any;
constructor(message: string, statusCode: number, response?: any);
}
// Network-related errors
class NetworkError extends HakgyoError {
constructor(message: string, code: string);
}Error Handling Example
import { AuthError, ApiError, NetworkError } from 'hakgyo-expo-sdk';
try {
await kelasApi.list();
} catch (error) {
if (error instanceof AuthError) {
// Handle authentication errors
console.error('Authentication failed:', error.message);
// Redirect to login
} else if (error instanceof ApiError) {
// Handle API errors
console.error(`API error (${error.statusCode}):`, error.message);
if (error.statusCode === 404) {
// Resource not found
} else if (error.statusCode >= 500) {
// Server error
}
} else if (error instanceof NetworkError) {
// Handle network errors
console.error('Network error:', error.message);
// Show offline message
} else {
// Unknown error
console.error('Unknown error:', error);
}
}Advanced Usage
Custom API Client
import { ApiClient } from 'hakgyo-expo-sdk';
const customClient = new ApiClient();
// Make custom requests
const data = await customClient.get<MyResponseType>('/custom-endpoint', {
page: 1,
limit: 20,
});Direct Auth Client Access
import { getAuthClient } from 'hakgyo-expo-sdk';
const authClient = getAuthClient();
// Access underlying better-auth client
const session = await authClient.getSession();Config Validation
import { validateConfig } from 'hakgyo-expo-sdk';
const config = {
baseURL: 'https://api.example.com',
// ... other config
};
const { valid, errors } = validateConfig(config);
if (!valid) {
console.error('Invalid config:', errors);
}API Endpoints Reference
| Module | Endpoint | Method | Description |
|--------|----------|--------|-------------|
| Auth | /api/auth/sign-up | POST | Register new user |
| Auth | /api/auth/sign-in | POST | Sign in user |
| Auth | /api/auth/sign-out | POST | Sign out user |
| Auth | /api/auth/session | GET | Get current session |
| Kelas | /api/kelas | GET | List all kelas |
| Kelas | /api/kelas | POST | Create new kelas |
| Kelas | /api/kelas/[id] | GET | Get specific kelas |
| Kelas | /api/kelas/[id] | PUT | Update kelas |
| Kelas | /api/kelas/[id] | DELETE | Delete kelas |
| Kelas | /api/kelas/[id]/join | POST | Join a kelas |
| Kelas | /api/kelas/[id]/leave | POST | Leave a kelas |
| Kelas | /api/kelas/[id]/materials | GET | Get kelas materials |
| Materi | /api/materi/[id] | GET | Get materi |
| Materi | /api/materi/[id]/complete | POST | Mark as complete |
| Materi | /api/materi/[id]/assessment | POST | Submit assessment |
| Materi | /api/materi/[id]/assessment-config | GET | Get assessment config |
| Vocabulary | /api/vocabulary/daily | GET | Get daily vocabulary |
| Vocabulary | /api/vocabulary-sets | GET | List vocabulary sets |
| Vocabulary | /api/vocabulary-sets | POST | Create vocabulary set |
| Vocabulary | /api/vocabulary-sets/[id] | GET | Get vocabulary set |
| Vocabulary | /api/vocabulary-sets/[id] | PUT | Update vocabulary set |
| Vocabulary | /api/vocabulary-sets/[id] | DELETE | Delete vocabulary set |
| Vocabulary | /api/vocabulary-items | GET | List vocabulary items |
| Vocabulary | /api/vocabulary-items | POST | Create vocabulary item |
| Vocabulary | /api/vocabulary-items/[id] | GET | Get vocabulary item |
| Vocabulary | /api/vocabulary-items/[id] | PUT | Update vocabulary item |
| Vocabulary | /api/vocabulary-items/[id] | DELETE | Delete vocabulary item |
| Vocabulary | /api/vocabulary-items/[id]/mark-learned | POST | Mark as learned |
| Soal | /api/soal-collections | GET | List question collections |
| Soal | /api/soal-collections | POST | Create collection |
| Soal | /api/soal-collections/[id] | GET | Get collection |
| Soal | /api/soal/practice | POST | Start practice session |
| Soal | /api/soal/practice/[id]/submit | POST | Submit practice |
| Tryout | /api/tryout | GET | List tryouts |
| Tryout | /api/tryout/active | GET | List active tryouts |
| Tryout | /api/tryout/[id] | GET | Get tryout |
| Tryout | /api/tryout/[id]/participate | POST | Participate in tryout |
| Tryout | /api/tryout/[id]/submit | POST | Submit tryout |
| Tryout | /api/tryout/[id]/results | GET | Get tryout results |
| Posts | /api/posts | GET | List posts |
| Posts | /api/posts | POST | Create post |
| Posts | /api/posts/[id] | GET | Get post |
| Posts | /api/posts/[id] | PUT | Update post |
| Posts | /api/posts/[id] | DELETE | Delete post |
| Posts | /api/posts/[id]/like | POST | Like post |
| Posts | /api/posts/[id]/unlike | POST | Unlike post |
| Posts | /api/posts/[id]/comments | GET | Get comments |
| Posts | /api/posts/[id]/comments | POST | Add comment |
| User | /api/users/me | GET | Get profile |
| User | /api/users/me | PUT | Update profile |
| User | /api/users/me/kelas | GET | Get user's classes |
| User | /api/users/me/tryout-results | GET | Get tryout results |
| User | /api/users/me/push-token | POST | Register push token |
| Push | /api/push-tokens/register | POST | Register push token |
| Push | /api/push-tokens/[id] | DELETE | Unregister push token |
License
MIT © [Your Name]
Support
For issues, questions, or contributions, please visit the GitHub repository.
