Comprehensive integration guide for implementing Abra Palabra's onboarding flow, dashboard, and all post-login features.
App Opens
│
├─ No Firebase session
│ └─→ LOGIN SCREEN (user signs in with email/password or OAuth)
│ └─→ Firebase Auth callback → POST /onboarding/begin
│
└─ Firebase session exists
│
├─ Get local cached onboardingStatus
│ │
│ ├─ onboardingStatus == COMPLETED
│ │ └─→ GET /user/dashboard → Load Home
│ │
│ ├─ onboardingStatus == IN_PROGRESS
│ │ └─→ GET /onboarding/:sessionId → Resume at currentStep
│ │
│ └─ onboardingStatus == NOT_STARTED
│ └─→ GET /onboarding/begin → Start Step 1
│
└─ Cache miss / first launch with session
└─→ GET /user/me → Check onboardingStatus field
└─→ Route as above
When: User first opens app, before Firebase login required.
POST /onboarding/begin
Content-Type: application/json
{
"interfaceLanguageCode": "en"
}
Response:
{
"statusCode": 200,
"data": {
"sessionId": "6641a1b2c3d4e5f6a7b8c9d0",
"sessionToken": "abc123eyJhbGciOi...",
"interfaceLanguage": {
"id": "lang_en",
"code": "en",
"name": "English"
}
}
}
What to do:
sessionId in secure local storage (required for all onboarding steps)interfaceLanguageCode for app UI localeFlutter Code:
Future<void> startOnboarding() async {
try {
final deviceLocale = Platform.localeName.split('_')[0]; // "en", "fr", etc.
final response = await http.post(
Uri.parse('$baseUrl/onboarding/begin'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'interfaceLanguageCode': deviceLocale}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)['data'];
// Save to local storage
await storage.write(key: 'onboard_sessionId', value: data['sessionId']);
await storage.write(key: 'interfaceLanguage', value: data['interfaceLanguage']['code']);
setState(() => sessionId = data['sessionId']);
}
} catch (e) {
showError('Failed to start onboarding: $e');
}
}
When: After user signs up with email/password and Firebase auth completes.
POST /onboarding/:sessionId/setup
Authorization: Bearer {firebaseToken}
Content-Type: application/json
{
"firstName": "Ana",
"lastName": "García",
"username": "anagarcia",
"learningLanguageCode": "es",
"proficiencyLevel": "BEGINNER",
"nativeLanguageCodes": ["en"],
"fcmToken": "{device-fcm-token}",
"referralCode": "A1B2C3"
}
Body Fields:
| Field | Type | Required | Notes |
|---|---|---|---|
firstName |
string | Yes | User's first name |
lastName |
string | Yes | User's last name |
username |
string | Yes | Unique username (validate availability first) |
learningLanguageCode |
string | Yes | Language to learn (e.g., "es", "fr", "de") |
proficiencyLevel |
enum | Yes | BEGINNER, INTERMEDIATE, or ADVANCED |
nativeLanguageCodes |
string[] | Yes | Languages user speaks (e.g., ["en"]) |
fcmToken |
string | Yes | Firebase Cloud Messaging token for push notifications |
referralCode |
string | No | Referral code if user was invited (optional) |
Response:
{
"statusCode": 200,
"data": {
"userId": "user_123abc",
"username": "anagarcia",
"email": "ana@example.com",
"learningLanguage": {
"code": "es",
"name": "Spanish",
"proficiencyLevel": "BEGINNER"
},
"nativeLanguages": ["en"],
"onboardingStatus": "IN_PROGRESS"
}
}
What to do:
userId and username in secure storageonboardingStatus = IN_PROGRESSError Cases:
| Status | Message | Action |
|---|---|---|
| 400 | Username already taken | Prompt user to choose different username |
| 409 | User already has this language | Show error and retry |
| 500 | FCM token invalid | Use null and retry; remind user to enable notifications later |
Flutter Code:
Future<void> setupOnboarding({
required String firstName,
required String lastName,
required String username,
required String languageCode,
required String proficiencyLevel,
required List<String> nativeLanguages,
String? referralCode,
}) async {
try {
final token = await auth.getIdToken();
final fcmToken = await messaging.getToken();
final response = await http.post(
Uri.parse('$baseUrl/onboarding/$sessionId/setup'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'firstName': firstName,
'lastName': lastName,
'username': username,
'learningLanguageCode': languageCode,
'proficiencyLevel': proficiencyLevel,
'nativeLanguageCodes': nativeLanguages,
'fcmToken': fcmToken,
if (referralCode != null) 'referralCode': referralCode,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)['data'];
await storage.write(key: 'userId', value: data['userId']);
await storage.write(key: 'username', value: data['username']);
await storage.write(key: 'onboardingStatus', value: 'IN_PROGRESS');
} else if (response.statusCode == 400) {
final error = jsonDecode(response.body)['message'];
if (error.contains('Username')) {
throw UsernameAlreadyTaken();
}
}
} catch (e) {
showError('Setup failed: $e');
rethrow;
}
}
When: After setup completes. Educational mini-game to introduce vocabulary.
GET /onboarding/challenge-words?language=es&size=6
Query Parameters:
| Parameter | Type | Default | Notes |
|---|---|---|---|
language |
string | Required | Language code (e.g. "es", "fr") |
size |
number | 6 | Number of word pairs (typically 6 for 12 cards) |
Response:
{
"statusCode": 200,
"data": [
{
"id": "card_001",
"word": "hola",
"translation": "hello",
"pairId": "pair_abc",
"side": "source"
},
{
"id": "card_002",
"word": "hello",
"translation": "hola",
"pairId": "pair_abc",
"side": "target"
},
...
]
}
How to implement in Flutter:
pairId), remove them from boardCard Matching Rules:
pairId = matchFlutter Game Logic:
class VocabularyGame {
List<GameCard> cards = [];
GameCard? firstFlipped;
int pairsMatched = 0;
int totalPairs = 0;
DateTime? startTime;
Future<void> loadGame(String languageCode) async {
final response = await http.get(
Uri.parse('$baseUrl/onboarding/challenge-words?language=$languageCode&size=6'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)['data'] as List;
cards = (data).map((c) => GameCard.fromJson(c)).toList();
cards.shuffle();
totalPairs = data.length ~/ 2;
startTime = DateTime.now();
}
}
void onCardTapped(GameCard card) {
if (card.isMatched || card.isFlipped) return;
card.flip();
if (firstFlipped == null) {
firstFlipped = card;
} else {
// Check for match
if (firstFlipped!.pairId == card.pairId) {
firstFlipped!.markMatched();
card.markMatched();
pairsMatched++;
firstFlipped = null;
} else {
// Mismatch - delay before flipping back
Future.delayed(Duration(milliseconds: 500), () {
firstFlipped!.flip();
card.flip();
firstFlipped = null;
});
}
}
}
bool get isGameComplete => pairsMatched == totalPairs;
}
When: After vocabulary game completes (or user skips it).
POST /onboarding/:sessionId/complete
Authorization: Bearer {firebaseToken}
Content-Type: application/json
{
"gameResults": {
"wordsMatched": 5,
"totalWords": 6,
"timeTakenSeconds": 42
}
}
Body Fields:
| Field | Type | Required | Notes |
|---|---|---|---|
wordsMatched |
number | No | Pairs successfully matched |
totalWords |
number | No | Total word pairs (usually 6) |
timeTakenSeconds |
number | No | Time in seconds to complete |
Response:
{
"statusCode": 200,
"data": {
"sessionId": "6641a1b2c3d4e5f6a7b8c9d0",
"userId": "user_123abc",
"completedAt": "2024-05-25T10:30:00Z",
"onboardingStatus": "COMPLETED"
}
}
What to do after:
onboardingStatus to COMPLETEDGET /user/dashboard)Flutter:
Future<void> completeOnboarding({
required int wordsMatched,
required int totalWords,
required int timeTakenSeconds,
}) async {
try {
final token = await auth.getIdToken();
final response = await http.post(
Uri.parse('$baseUrl/onboarding/$sessionId/complete'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'gameResults': {
'wordsMatched': wordsMatched,
'totalWords': totalWords,
'timeTakenSeconds': timeTakenSeconds,
},
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)['data'];
await storage.write(key: 'onboardingStatus', value: 'COMPLETED');
// Navigate to dashboard
Navigator.of(context).pushReplacementNamed('/dashboard');
}
} catch (e) {
showError('Failed to complete onboarding: $e');
}
}
When: App launches and user has in-progress onboarding.
GET /onboarding/:sessionId
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"sessionId": "session_123",
"userId": "user_123abc",
"currentStep": 2,
"status": "IN_PROGRESS",
"createdAt": "2024-05-25T10:00:00Z",
"completedSteps": ["language_selection", "setup"]
}
}
What to do:
currentStepWhen: App launches after onboarding, or user navigates to Home tab.
GET /user/dashboard
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"user": {
"userId": "user_123",
"username": "anagarcia",
"firstName": "Ana",
"lastName": "García",
"avatar": "https://cdn.example.com/avatars/ana.jpg",
"email": "ana@example.com",
"tier": "FREE"
},
"activeLanguage": {
"code": "es",
"name": "Spanish"
},
"xp": {
"totalXp": 2450,
"weeklyXp": 380,
"level": "INTERMEDIATE",
"progress": 65,
"xpToNextLevel": 450
},
"streak": {
"current": 7,
"best": 34,
"freezeTokens": 1,
"freezeTokenCost": 200,
"isActive": true,
"lastUpdated": "2024-05-25T08:15:00Z"
},
"weeklyActivity": [
{ "day": "Mon", "date": "2024-05-20", "active": true },
{ "day": "Tue", "date": "2024-05-21", "active": true },
{ "day": "Wed", "date": "2024-05-22", "active": false },
{ "day": "Thu", "date": "2024-05-23", "active": true },
{ "day": "Fri", "date": "2024-05-24", "active": false },
{ "day": "Sat", "date": "2024-05-25", "active": true },
{ "day": "Sun", "date": "2024-05-26", "active": false }
],
"nextLesson": {
"lessonId": "lesson_456",
"title": "Travel Phrases",
"level": "BEGINNER",
"unit": { "id": "unit_20", "title": "Unit 3 — On the Move" },
"status": "IN_PROGRESS",
"currentStep": 2,
"totalSteps": 8,
"nextAction": "continue"
},
"dailyChallenges": [
{
"id": "challenge_001",
"title": "Vocabulary Sprint",
"type": "VOCABULARY_SPRINT",
"difficulty": 2,
"status": "NOT_STARTED",
"completed": false,
"xpReward": 20,
"timeLeft": { "hours": 14, "minutes": 30, "label": "14h 30m left" }
}
],
"teamQuest": {
"sessionId": "collab_123",
"partner": {
"id": "user_789",
"username": "gildas",
"firstName": "Gildas",
"avatar": "https://..."
},
"lessonTitle": "Present Tense",
"progress": 0.4,
"myProgress": { "completed": 3, "total": 8 },
"partnerProgress": { "completed": 2, "total": 8 },
"startedAt": "2024-05-21T18:00:00Z"
},
"communityHighlight": {
"postId": "post_999",
"type": "LANGUAGE_TIP",
"title": "Preterite vs Imperfect",
"author": {
"id": "user_111",
"username": "leo_expert",
"firstName": "Leo",
"avatar": "https://..."
},
"likes": 42,
"comments": 8
},
"notifications": [
{
"id": "notif_001",
"type": "STREAK_REMINDER",
"title": "7-Day Streak! 🔥",
"message": "You've maintained a 7-day streak. Keep it up!",
"read": false,
"createdAt": "2024-05-25T08:00:00Z"
}
]
}
}
What each section displays:
| Section | Purpose | UI | Tap Action |
|---|---|---|---|
user |
User profile header (avatar, name, tier) | Top card with avatar | → /profile/me |
activeLanguage |
Current learning language | Language badge | → Language switcher |
xp |
Level progress bar | Progress ring + label | → Activity screen |
streak |
Fire icon with current streak | Fire icon + number | → Buy freeze token |
weeklyActivity |
7-day activity dots | Dots row | Read-only |
nextLesson |
CTA button "Resume Lesson" | Blue button | → Start lesson |
dailyChallenges |
1-3 daily challenges | Card stack | → Challenge screen |
teamQuest |
Active collaboration | Card with partner | → Collaboration session |
communityHighlight |
Featured post | Card preview | → Full post |
notifications |
Recent notifications bell | Bell icon + count | → Notifications list |
Flutter Implementation:
Future<Dashboard> fetchDashboard() async {
final token = await auth.getIdToken();
final response = await http.get(
Uri.parse('$baseUrl/user/dashboard'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
return Dashboard.fromJson(jsonDecode(response.body)['data']);
}
throw Exception('Failed to load dashboard');
}
// Build dashboard widgets
class DashboardScreen extends StatefulWidget {
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
late Future<Dashboard> dashboardFuture;
@override
void initState() {
super.initState();
dashboardFuture = fetchDashboard();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Dashboard>(
future: dashboardFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.loading) {
return LoadingScreen();
}
if (snapshot.hasError) {
return ErrorScreen(error: snapshot.error.toString());
}
final dashboard = snapshot.data!;
return ListView(
children: [
// Header with user profile
UserProfileCard(user: dashboard.user),
// XP / Level progress
XpProgressCard(xp: dashboard.xp),
// Streak
StreakCard(streak: dashboard.streak),
// Weekly activity
WeeklyActivityWidget(activity: dashboard.weeklyActivity),
// Next lesson CTA
if (dashboard.nextLesson != null)
NextLessonCard(lesson: dashboard.nextLesson!),
// Daily challenges
if (dashboard.dailyChallenges.isNotEmpty)
DailyChallengesWidget(challenges: dashboard.dailyChallenges),
// Team quest
if (dashboard.teamQuest != null)
TeamQuestCard(quest: dashboard.teamQuest!),
// Community highlight
if (dashboard.communityHighlight != null)
CommunityHighlightCard(post: dashboard.communityHighlight!),
],
);
},
);
}
}
When: App launches to check user status, or user views their profile.
GET /user/me
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"userId": "user_123",
"username": "anagarcia",
"email": "ana@example.com",
"firebaseUid": "firebase_uid_...",
"firstName": "Ana",
"lastName": "García",
"avatar": "https://...",
"tier": "FREE",
"status": "ACTIVE",
"createdAt": "2024-01-15T10:00:00Z",
"onboardingStatus": "COMPLETED",
"referralCode": "A1B2C3",
"activeLearningLanguage": {
"code": "es",
"name": "Spanish"
},
"nativeLanguages": [
{ "code": "en", "name": "English" },
{ "code": "fr", "name": "French" }
],
"interests": [
{ "id": "interest_001", "name": "Travel" },
{ "id": "interest_002", "name": "Business" }
],
"preferences": {
"notifications": {
"pushEnabled": true,
"emailEnabled": false,
"streakReminder": true,
"dailyReminder": true,
"followingActivity": true,
"friendRequest": true,
"collaborationRequest": true,
"groupInvite": true
},
"learning": {
"studyTime": "morning",
"difficultyLevel": "INTERMEDIATE",
"lessonRecallFrequency": 2
},
"community": {
"profileVisibility": "PUBLIC",
"allowFriendRequests": true,
"blockList": []
},
"accessibility": {
"fontSize": 16,
"darkMode": false,
"reduceMotion": false
}
},
"stats": {
"lessonsCompleted": 42,
"currentStreak": 7,
"bestStreak": 34,
"totalXp": 2450,
"wordsLearned": 156
}
}
}
Flutter:
Future<User> fetchUserProfile() async {
final token = await auth.getIdToken();
final response = await http.get(
Uri.parse('$baseUrl/user/me'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body)['data']);
}
throw Exception('Failed to fetch user profile');
}
PATCH /user/me/profile
Authorization: Bearer {firebaseToken}
{
"firstName": "Ana",
"lastName": "García",
"avatar": "base64_or_url",
"bio": "Language enthusiast from Spain"
}
PATCH /user/me/interests
Authorization: Bearer {firebaseToken}
{
"interests": ["interest_001", "interest_002", "interest_003"]
}
PATCH /user/me/interface-language
Authorization: Bearer {firebaseToken}
{
"interfaceLanguageCode": "en"
}
DELETE /user/me/account
Authorization: Bearer {firebaseToken}
GET /settings
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"notifications": {
"pushEnabled": true,
"emailEnabled": false,
"streakReminder": true,
"dailyReminder": true,
"withNewsletter": false,
"followingActivity": true,
"friendRequest": true,
"collaborationRequest": true,
"groupInvite": true
},
"learning": {
"studyTime": "morning",
"difficultyLevel": "INTERMEDIATE",
"lessonRecallFrequency": 2,
"enableOffline": true
},
"community": {
"profileVisibility": "PUBLIC",
"allowFriendRequests": true,
"blockList": ["user_blocked_123"]
},
"accessibility": {
"fontSize": 16,
"darkMode": false,
"reduceMotion": false,
"enableCaptions": true
}
}
}
PATCH /settings/notifications
Authorization: Bearer {firebaseToken}
{
"pushEnabled": true,
"streakReminder": true,
"dailyReminder": true,
"followingActivity": true,
"friendRequest": true
}
PATCH /settings/learning
Authorization: Bearer {firebaseToken}
{
"studyTime": "morning",
"difficultyLevel": "INTERMEDIATE",
"lessonRecallFrequency": 2
}
Options:
studyTime: "morning" | "afternoon" | "evening"difficultyLevel: "BEGINNER" | "INTERMEDIATE" | "ADVANCED"lessonRecallFrequency: 1–5 (days between recalls)PATCH /settings/community
Authorization: Bearer {firebaseToken}
{
"profileVisibility": "PUBLIC",
"allowFriendRequests": true,
"blockList": ["user_id_to_block"]
}
Options:
profileVisibility: "PUBLIC" | "PRIVATE" | "FRIENDS_ONLY"PATCH /settings/accessibility
Authorization: Bearer {firebaseToken}
{
"fontSize": 16,
"darkMode": false,
"reduceMotion": false,
"enableCaptions": true
}
When: User taps to switch languages or view all courses.
GET /user/courses
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"courses": [
{
"id": "ulp_es_123",
"languageCode": "es",
"languageName": "Spanish",
"isActive": true,
"proficiencyLevel": "INTERMEDIATE",
"level": 12,
"totalLevels": 24,
"progress": 50,
"learnedWords": 203,
"targetWords": 400,
"streak": {
"current": 7,
"best": 34,
"isActive": true,
"lastUpdated": "2024-05-25T08:00:00Z"
}
},
{
"id": "ulp_fr_456",
"languageCode": "fr",
"languageName": "French",
"isActive": false,
"proficiencyLevel": "BEGINNER",
"level": 2,
"totalLevels": 24,
"progress": 5,
"learnedWords": 18,
"targetWords": 400,
"streak": {
"current": 0,
"best": 5,
"isActive": false,
"lastUpdated": "2024-05-20T10:00:00Z"
}
}
],
"explore": [
{ "code": "de", "name": "German" },
{ "code": "it", "name": "Italian" },
{ "code": "ja", "name": "Japanese" }
],
"milestone": {
"title": "Unlock ADVANCED Spanish",
"description": "You're 50% of the way. 197 more words to unlock advanced.",
"progressPct": 50,
"currentLevel": "INTERMEDIATE",
"nextLevel": "ADVANCED",
"languageCode": "es",
"languageName": "Spanish"
}
}
}
PATCH /user/me/active-language
Authorization: Bearer {firebaseToken}
{
"languageCode": "fr"
}
Step 1: Select motivation
GET /language/learning-motivations
Response:
{
"statusCode": 200,
"data": [
{ "id": "education", "label": "Education goals", "icon": "school" },
{ "id": "challenge", "label": "Challenge myself", "icon": "lightbulb" },
{ "id": "travel", "label": "Travelling", "icon": "flight" },
{ "id": "culture", "label": "Culture connection", "icon": "public" },
{ "id": "prior_knowledge", "label": "I already know some", "icon": "star" }
]
}
Step 2: Enroll user
POST /user/start-language
Authorization: Bearer {firebaseToken}
{
"languageCode": "fr",
"motivation": "travel"
}
Response:
{
"statusCode": 200,
"data": {
"languageCode": "fr",
"alreadyEnrolled": false,
"message": "French enrollment created. Proceed to placement."
}
}
Get quiz questions
GET /user/placement-quiz/:languageCode?round=1&retake=false
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"round": 1,
"totalQuestions": 5,
"questions": [
{
"questionId": "q_001",
"type": "MULTIPLE_CHOICE",
"question": "What is the Spanish word for 'hello'?",
"options": ["adiós", "hola", "buenos días", "qué tal"],
"difficultyLevel": "BEGINNER"
}
]
}
}
Submit answers
POST /user/placement-result/:languageCode
Authorization: Bearer {firebaseToken}
{
"round": 1,
"answers": [
{ "questionId": "q_001", "selectedIndex": 1 },
{ "questionId": "q_002", "selectedIndex": 2 },
...
]
}
Response:
{
"statusCode": 200,
"data": {
"round": 1,
"correct": 4,
"total": 5,
"passed": true,
"level": "BEGINNER",
"nextAction": "next_round",
"nextRound": 2,
"feedback": [
{
"questionId": "q_001",
"selectedIndex": 1,
"correctIndex": 1,
"correctAnswer": "hola",
"isCorrect": true
}
]
}
}
Scoring Logic:
| Round | Difficulty | Fail → Level | Pass → |
|---|---|---|---|
| 1 | BEGINNER | BEGINNER | Round 2 |
| 2 | INTERMEDIATE | INTERMEDIATE | Round 3 |
| 3 | ADVANCED | ADVANCED | Complete |
GET /user/lesson-screen
Authorization: Bearer {firebaseToken}
Response: (See Part B of original doc - section repeats: daily goal, continue lesson, vocabulary, recommended, challenges)
GET /user/topics
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": [
{ "id": "topic_001", "name": "food", "label": "Food & Drinks" },
{ "id": "topic_002", "name": "travel", "label": "Travel" },
{ "id": "topic_003", "name": "business", "label": "Business" },
{ "id": "topic_004", "name": "culture", "label": "Culture" }
]
}
GET /user/topic/:topicId
Authorization: Bearer {firebaseToken}
GET /user/daily-goal
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"goalType": "FLASHCARDS",
"targetCount": 5,
"progress": 2,
"completed": false,
"date": "2024-05-25T00:00:00Z"
}
}
POST /user/daily-goal
Authorization: Bearer {firebaseToken}
{
"goalType": "LESSONS",
"targetCount": 3
}
Options:
goalType: "FLASHCARDS" | "LESSONS" | "CHALLENGES" | "VOCABULARY" | "MINUTES"targetCount: 1–100GET /user/activity?year=2024&month=5
Authorization: Bearer {firebaseToken}
Response:
{
"statusCode": 200,
"data": {
"lifetimeStats": {
"longestStreak": 124,
"currentStreak": 7,
"streakActive": true,
"lessonsCompleted": 350,
"totalXpEarned": 15200
},
"recentActivity": [
{
"id": "act_001",
"type": "LESSON_COMPLETED",
"label": "Completed a lesson",
"detail": "Travel Phrases",
"timestamp": "2024-05-25T10:00:00Z"
},
{
"id": "act_002",
"type": "LEVEL_UP",
"label": "Leveled up!",
"detail": "Reached INTERMEDIATE",
"timestamp": "2024-05-24T15:30:00Z"
}
],
"calendar": {
"year": 2024,
"month": 5,
"activeDays": {
"1": false, "2": false, ..., "22": true, "23": true, ...
}
},
"badges": [
{
"id": "badge_001",
"name": "Active Learner",
"description": "Completed 7 days in a row",
"iconUrl": "https://...",
"earnedAt": "2024-05-15T00:00:00Z"
}
]
}
}
| Status | Scenario | Action |
|---|---|---|
| 200 | Success | Proceed normally |
| 201 | Created | Proceed normally (e.g., onboarding started) |
| 400 | Bad Request | Invalid parameters; show user-friendly error |
| 401 | Unauthorized | Token expired; refresh and retry |
| 404 | Not Found | Resource doesn't exist; show error |
| 409 | Conflict | Duplicate (username, already learning language); prompt user |
| 500 | Server Error | Retry with exponential backoff; show "try again" |
Firebase tokens expire after 1 hour. Implement auto refresh:
Future<String> getValidToken() async {
var token = await storage.read(key: 'firebaseToken');
final expires = await storage.read(key: 'tokenExpiry');
if (expires != null) {
final expiry = DateTime.parse(expires);
if (DateTime.now().isAfter(expiry.subtract(Duration(minutes: 5)))) {
// Token expiring soon, refresh
token = await FirebaseAuth.instance.currentUser?.getIdToken(true);
await storage.write(key: 'firebaseToken', value: token!);
await storage.write(
key: 'tokenExpiry',
value: DateTime.now().add(Duration(hours: 1)).toIso8601String(),
);
}
}
return token!;
}
Future<T> apiCall<T>(
Future<http.Response> Function() request,
T Function(dynamic) parser,
) async {
int retries = 0;
const maxRetries = 3;
const backoffBase = Duration(milliseconds: 500);
while (retries < maxRetries) {
try {
final response = await request().timeout(
Duration(seconds: 30),
onTimeout: () => throw TimeoutException(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return parser(jsonDecode(response.body)['data']);
} else if (response.statusCode == 401) {
// Refresh token and retry
await getValidToken();
continue; // Retry request
} else if (response.statusCode >= 400 && response.statusCode < 500) {
throw ClientException(jsonDecode(response.body)['message']);
} else {
throw ServerException();
}
} on SocketException {
if (retries < maxRetries - 1) {
await Future.delayed(backoffBase * pow(2, retries));
retries++;
} else {
throw NetworkException('No internet connection');
}
} on TimeoutException {
if (retries < maxRetries - 1) {
await Future.delayed(backoffBase * pow(2, retries));
retries++;
} else {
throw NetworkException('Request timeout');
}
}
}
throw Exception('Unknown error occurred');
}
class OfflineSyncManager {
Future<void> setUpOfflineSync() async {
// Cache dashboard on successful fetch
connectivity.onConnectivityChanged.listen((result) {
if (result == ConnectivityResult.none) {
_goOfflineMode();
} else {
_syncPendingRequests();
}
});
}
Future<void> _syncPendingRequests() async {
// Sync queued actions: lessons started, goals set, etc.
final queue = await _getOfflineQueue();
for (final request in queue) {
try {
await apiCall(() => request.execute());
await _removeFromQueue(request.id);
} catch (e) {
// Retry next time
}
}
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<AppState> appStateFuture;
@override
void initState() {
super.initState();
appStateFuture = _determineRoute();
}
Future<AppState> _determineRoute() async {
// Step 1: Check Firebase auth
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
return AppState.loginScreen;
}
// Step 2: Check local onboarding status
final storage = FlutterSecureStorage();
final onboardingStatus = await storage.read(key: 'onboardingStatus');
if (onboardingStatus == 'COMPLETED') {
return AppState.dashboardScreen;
} else if (onboardingStatus == 'IN_PROGRESS') {
final sessionId = await storage.read(key: 'onboard_sessionId');
final stepData = await _resumeOnboarding(sessionId!);
return AppState.resumeOnboardingStep(stepData['currentStep']);
} else {
return AppState.onboardingStep1;
}
}
Future<dynamic> _resumeOnboarding(String sessionId) async {
final token = await FirebaseAuth.instance.currentUser!.getIdToken();
final response = await http.get(
Uri.parse('$baseUrl/onboarding/$sessionId'),
headers: {'Authorization': 'Bearer $token'},
);
return jsonDecode(response.body)['data'];
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder<AppState>(
future: appStateFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return SplashScreen();
}
if (snapshot.hasError) {
return ErrorScreen(error: snapshot.error.toString());
}
final state = snapshot.data!;
switch (state.type) {
case 'login':
return LoginScreen();
case 'onboarding_step_1':
return OnboardingLanguageStep();
case 'onboarding_step_2':
return OnboardingSetupStep();
case 'onboarding_step_3':
return OnboardingVocabularyGame();
case 'dashboard':
return DashboardScreen();
default:
return SplashScreen();
}
},
),
);
}
}
class OnboardingController extends ChangeNotifier {
String? sessionId;
late FirebaseAuth auth;
late FlutterSecureStorage storage;
OnboardingController() {
auth = FirebaseAuth.instance;
storage = FlutterSecureStorage();
}
// Step 1
Future<void> beginOnboarding(String languageCode) async {
const baseUrl = 'https://api.abrainnovations.io/v1';
final response = await http.post(
Uri.parse('$baseUrl/onboarding/begin'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'interfaceLanguageCode': languageCode}),
);
final data = jsonDecode(response.body)['data'];
sessionId = data['sessionId'];
await storage.write(key: 'onboard_sessionId', value: sessionId!);
await storage.write(key: 'interfaceLanguage', value: languageCode);
notifyListeners();
}
// Step 2
Future<void> setupOnboarding({
required String firstName,
required String lastName,
required String username,
required String learningLanguageCode,
required String proficiencyLevel,
required List<String> nativeLanguageCodes,
String? referralCode,
}) async {
const baseUrl = 'https://api.abrainnovations.io/v1';
final token = await auth.currentUser!.getIdToken();
final fcmToken = await FirebaseMessaging.instance.getToken();
final response = await http.post(
Uri.parse('$baseUrl/onboarding/$sessionId/setup'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'firstName': firstName,
'lastName': lastName,
'username': username,
'learningLanguageCode': learningLanguageCode,
'proficiencyLevel': proficiencyLevel,
'nativeLanguageCodes': nativeLanguageCodes,
'fcmToken': fcmToken,
if (referralCode != null) 'referralCode': referralCode,
}),
);
final data = jsonDecode(response.body)['data'];
await storage.write(key: 'userId', value: data['userId']);
await storage.write(key: 'username', value: data['username']);
notifyListeners();
}
// Step 4
Future<void> completeOnboarding({
required int wordsMatched,
required int totalWords,
required int timeTakenSeconds,
}) async {
const baseUrl = 'https://api.abrainnovations.io/v1';
final token = await auth.currentUser!.getIdToken();
final response = await http.post(
Uri.parse('$baseUrl/onboarding/$sessionId/complete'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'gameResults': {
'wordsMatched': wordsMatched,
'totalWords': totalWords,
'timeTakenSeconds': timeTakenSeconds,
},
}),
);
await storage.write(key: 'onboardingStatus', value: 'COMPLETED');
sessionId = null;
notifyListeners();
}
}
class DashboardProvider extends ChangeNotifier {
Dashboard? dashboard;
bool isLoading = false;
Future<void> fetchDashboard() async {
isLoading = true;
notifyListeners();
try {
const baseUrl = 'https://api.abrainnovations.io/v1';
final token = await FirebaseAuth.instance.currentUser!.getIdToken();
final response = await http.get(
Uri.parse('$baseUrl/user/dashboard'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
dashboard = Dashboard.fromJson(
jsonDecode(response.body)['data'],
);
}
} catch (e) {
// Show error
} finally {
isLoading = false;
notifyListeners();
}
}
}
class DashboardScreen extends StatefulWidget {
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
late DashboardProvider provider;
@override
void initState() {
super.initState();
provider = context.read<DashboardProvider>();
provider.fetchDashboard();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Abra Palabra')),
body: Consumer<DashboardProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return LoadingScreen();
}
if (provider.dashboard == null) {
return ErrorScreen();
}
return RefreshIndicator(
onRefresh: () => provider.fetchDashboard(),
child: _buildDashboardContent(provider.dashboard!),
);
},
),
);
}
Widget _buildDashboardContent(Dashboard dashboard) {
return ListView(
padding: EdgeInsets.all(16),
children: [
// User header
UserProfileCard(user: dashboard.user),
SizedBox(height: 16),
// XP + Level
XpCard(xp: dashboard.xp),
SizedBox(height: 16),
// Streak
StreakCard(
streak: dashboard.streak,
onFreezeTokenTap: () => _showBuyFreezeDialog(context),
),
SizedBox(height: 16),
// Weekly activity
WeeklyActivityChart(dashboard.weeklyActivity),
SizedBox(height: 24),
// CTA: Resume lesson
if (dashboard.nextLesson != null)
ElevatedButton.icon(
onPressed: () => launchLesson(dashboard.nextLesson!.lessonId),
icon: Icon(Icons.play_arrow),
label: Text('Resume: ${dashboard.nextLesson!.title}'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blueAccent,
),
),
SizedBox(height: 24),
// Daily challenges
if (dashboard.dailyChallenges.isNotEmpty) ...[
Text('Today's Challenges', style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 12),
...dashboard.dailyChallenges
.map((c) => ChallengeCard(challenge: c))
.toList(),
SizedBox(height: 24),
],
// Team quest
if (dashboard.teamQuest != null) ...[
TeamQuestCard(quest: dashboard.teamQuest!),
SizedBox(height: 24),
],
// Community highlight
if (dashboard.communityHighlight != null)
CommunityCard(post: dashboard.communityHighlight!),
],
);
}
void _showBuyFreezeDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Buy Streak Freeze?'),
content: Text('Use 200 XP to protect your streak?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
// Call BUY freeze API
Navigator.pop(context);
},
child: Text('Buy'),
),
],
),
);
}
}
Base URL:
https://api.abrainnovations.io (production)
All endpoints use URI versioning: /v1/...
Last Updated: May 2024
This completes the comprehensive onboarding + dashboard + settings integration guide for Flutter developers.