Compare commits
16 Commits
9696eadeaf
...
bfb072ddfb
| Author | SHA1 | Date | |
|---|---|---|---|
| bfb072ddfb | |||
| 387f0f65de | |||
| 75343f97b6 | |||
| 92da8b2013 | |||
| 82c9b1eee8 | |||
| a8c20d929e | |||
| 1653482d54 | |||
| 9de6b8a78f | |||
| 9fa4724b2a | |||
| 31da57d218 | |||
| 10699d1ad1 | |||
| e4bffe1a71 | |||
| 368d18a0bf | |||
| 34a8f7154e | |||
| fbc94daac7 | |||
| fcb8eaa505 |
19
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.log
|
||||
.dart_tool/
|
||||
.packages
|
||||
@@ -11,6 +13,23 @@ build/
|
||||
.flutter-plugins-dependencies
|
||||
bugreport-*.zip
|
||||
|
||||
# Claude per-project agent memory (local-only, machine-specific) — nested
|
||||
# patterns need the `**/` prefix to match below the repo root (e.g.
|
||||
# backend/.claude/agent-memory/).
|
||||
**/.claude/agent-memory/
|
||||
|
||||
# Maestro local debug artifacts (screenshots dumped at app root by
|
||||
# `--debug-output` / takeScreenshot; results journal regenerated each run)
|
||||
mitra_app/*.png
|
||||
mitra_app/.maestro/RESULTS.md
|
||||
client_app/*.png
|
||||
client_app/.maestro/RESULTS.md
|
||||
|
||||
# Stray google-services.json at repo root (real ones live under
|
||||
# {client_app,mitra_app}/android/app/). Usually a `flutterfire configure`
|
||||
# misdrop — keep local but don't commit.
|
||||
/google-services.json
|
||||
|
||||
# Figma design dump (do not check in)
|
||||
requirement/Figma.zip
|
||||
requirement/Figma/
|
||||
|
||||
@@ -48,3 +48,22 @@ Internal listener must never be exposed to the public internet.
|
||||
- Use Fastify plugins for shared middleware (auth, error handling, logging)
|
||||
- Business logic lives in `services/` — never directly in route handlers
|
||||
- Never reintroduce Firebase Auth. `firebase-admin` is FCM-only; do not import `.auth()` from it.
|
||||
|
||||
## Config-Source Convention
|
||||
|
||||
Two distinct knob-types exist; do not conflate them:
|
||||
|
||||
- **DB-stored** (`app_config` table, mutable via CC SettingsPage at runtime): used for operator-tunable values that may change between deploys without a code roll — `mitra_stale_after_seconds`, `extension_timeout_seconds`, `pricing_tiers`, `support_handles_json`, `max_customers_per_mitra`, etc. Read via getters in `services/config.service.js`. Cache invalidation goes through `valkey` pub/sub when needed.
|
||||
- **Env-driven** (`process.env`, set per deployment via `.env` or Cloud Run env vars): used for deploy-fixed values that should never differ between operator actions — `MITRA_HEARTBEAT_CADENCE_SECONDS`, `FIREBASE_SERVICE_ACCOUNT_PATH`, `AUTH_JWT_SECRET`, `DATABASE_URL`. Always expose via a getter helper with a sane default + numeric parsing (see `getMitraHeartbeatCadenceSeconds` in config.service.js for the pattern).
|
||||
|
||||
When a new value needs to flow from CC → app, prefer DB. When it's a deploy-fixed contract (e.g. heartbeat cadence the apps must honor, Xendit credentials, callback tokens), prefer env. CC inputs that depend on env values (e.g. min/max validation) read the env-derived value via the same config endpoint that surfaces the DB value, and the PATCH route validates against it.
|
||||
|
||||
## FCM Channel Convention
|
||||
|
||||
Single channel `halobestie_chat_v1` is shared by both apps (registered in each app's `core/notifications/notification_service.dart`) and ships the branded `halobestie_notif.ogg` sound. Backend FCM payloads should always target this channel ID via `android.notification.channelId`:
|
||||
|
||||
```js
|
||||
android: { priority: 'high', notification: { channelId: 'halobestie_chat_v1' } }
|
||||
```
|
||||
|
||||
Do not introduce per-recipient or per-feature channels lightly. If a new sound is required (e.g. payment alert), bump the channel ID (`halobestie_chat_v2`) and update both apps simultaneously — Android binds channel sound at create-time on API 26+, so mutating the existing channel doesn't pick up the new sound for installed users.
|
||||
|
||||
@@ -13,7 +13,7 @@ import { clientPaymentRoutes } from './routes/public/client.payment.routes.js'
|
||||
import { clientMitraAvailabilityRoutes } from './routes/public/client.mitra-availability.routes.js'
|
||||
import { publicBestieAvailabilityRoutes } from './routes/public/public.bestie-availability.routes.js'
|
||||
import { clientOnboardingRoutes } from './routes/public/client.onboarding.routes.js'
|
||||
import { clientSupportRoutes } from './routes/public/client.support.routes.js'
|
||||
import { sharedSupportRoutes } from './routes/public/shared.support.routes.js'
|
||||
import { sharedChatRoutes } from './routes/public/shared.chat.routes.js'
|
||||
import { errorHandler } from './plugins/error-handler.js'
|
||||
import { registerWebSocketPlugin, registerWebSocketRoute } from './plugins/websocket.js'
|
||||
@@ -38,10 +38,10 @@ export const buildPublicApp = async () => {
|
||||
app.register(clientPaymentRoutes, { prefix: '/api/client/payment-sessions' })
|
||||
app.register(clientMitraAvailabilityRoutes, { prefix: '/api/client/mitra-availability' })
|
||||
app.register(publicBestieAvailabilityRoutes, { prefix: '/api/public/bestie' })
|
||||
// Phase 4: onboarding-state + support handles. Both are tiny so they live in their
|
||||
// own files rather than bloating client.auth.routes / shared.config.routes.
|
||||
// Onboarding-state stays client-only (anonymous customer flow). Support
|
||||
// handles are shared — both client and mitra apps link the same WA/TG.
|
||||
app.register(clientOnboardingRoutes, { prefix: '/api/client' })
|
||||
app.register(clientSupportRoutes, { prefix: '/api/client' })
|
||||
app.register(sharedSupportRoutes, { prefix: '/api/shared' })
|
||||
|
||||
// WebSocket route (registered at app level, not prefixed)
|
||||
registerWebSocketRoute(app)
|
||||
|
||||
@@ -295,6 +295,18 @@ const migrate = async () => {
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`
|
||||
|
||||
// Mitra reachability — replaces the implicit `ping_interval * 3` grace
|
||||
// window with an operator-facing "max heartbeat age" knob. The companion
|
||||
// heartbeat cadence lives in env (MITRA_HEARTBEAT_CADENCE_SECONDS, default
|
||||
// 30s). Default 45s keeps the same effective grace as the old 15s ping × 3.
|
||||
// `mitra_ping_interval_seconds` is left in place (vestigial) — no live code
|
||||
// path reads it anymore; safe to drop after one release.
|
||||
await sql`
|
||||
INSERT INTO app_config (key, value)
|
||||
VALUES ('mitra_stale_after_seconds', '{"value": 45}')
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`
|
||||
|
||||
// --- Phase 3.2: Mitra Request Activity Log ---
|
||||
|
||||
await sql`
|
||||
|
||||
@@ -442,6 +442,26 @@ export const internalTestRoutes = async (fastify) => {
|
||||
return { ok: true, ...updated }
|
||||
})
|
||||
|
||||
// Delete the mitra_online_status row for a given mitra — used by Maestro
|
||||
// scenario flows that need to simulate a "freshly created mitra with NO
|
||||
// status row yet" (the natural state right after seed_mitra and before
|
||||
// any /api/mitra/status call from the app). The app's first /status call
|
||||
// re-creates the row via ensureStatusRow() with the DB default
|
||||
// is_online=false; this endpoint just rewinds to that pre-state.
|
||||
//
|
||||
// Body: { mitra_id }
|
||||
fastify.post('/delete-mitra-status-row', async (request, reply) => {
|
||||
const mitraId = request.body?.mitra_id
|
||||
if (!mitraId) {
|
||||
return reply.code(400).send({ error: 'mitra_id required in body' })
|
||||
}
|
||||
const result = await sql`
|
||||
DELETE FROM mitra_online_status WHERE mitra_id = ${mitraId}
|
||||
RETURNING mitra_id
|
||||
`
|
||||
return { ok: true, mitra_id: mitraId, deleted: result.length > 0 }
|
||||
})
|
||||
|
||||
// Accept the most recent pending pairing notification, regardless of which
|
||||
// mitra it was sent to. Used by Maestro flows where the test doesn't know
|
||||
// (or care) which specific mitra should accept — e.g. TS-02 (blast where
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getFreeTrialConfig, setFreeTrialConfig,
|
||||
getExtensionTimeoutConfig, setExtensionTimeoutConfig,
|
||||
getEarlyEndConfig, setEarlyEndConfig,
|
||||
getMitraPingConfig, setMitraPingConfig,
|
||||
getMitraPingConfig, setMitraPingConfig, getMitraHeartbeatCadenceSeconds,
|
||||
getSensitivityConfig, setSensitivityConfig,
|
||||
getPaymentSessionTimeoutMinutes, setPaymentSessionTimeoutMinutes,
|
||||
getReturningChatConfirmationTimeoutSeconds, setReturningChatConfirmationTimeoutSeconds,
|
||||
@@ -173,14 +173,23 @@ export const internalConfigRoutes = async (app) => {
|
||||
app.patch('/mitra-ping', {
|
||||
preHandler: [authenticate, attachCcUser, requirePermission('config', 'update')],
|
||||
}, async (request, reply) => {
|
||||
const { require_ping, ping_interval_seconds } = request.body ?? {}
|
||||
const { require_ping, stale_after_seconds } = request.body ?? {}
|
||||
if (require_ping !== undefined && typeof require_ping !== 'boolean') {
|
||||
return reply.code(422).send({ success: false, error: { code: 'VALIDATION_ERROR', message: 'require_ping must be a boolean' } })
|
||||
}
|
||||
if (ping_interval_seconds !== undefined && (typeof ping_interval_seconds !== 'number' || ping_interval_seconds < 5)) {
|
||||
return reply.code(422).send({ success: false, error: { code: 'VALIDATION_ERROR', message: 'ping_interval_seconds must be a number >= 5' } })
|
||||
if (stale_after_seconds !== undefined) {
|
||||
const cadence = getMitraHeartbeatCadenceSeconds()
|
||||
if (typeof stale_after_seconds !== 'number' || stale_after_seconds < cadence) {
|
||||
return reply.code(422).send({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: `stale_after_seconds must be a number >= heartbeat cadence (${cadence}s)`,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
const config = await setMitraPingConfig({ require_ping, ping_interval_seconds })
|
||||
const config = await setMitraPingConfig({ require_ping, stale_after_seconds })
|
||||
return reply.send({ success: true, data: config })
|
||||
})
|
||||
|
||||
|
||||
@@ -56,7 +56,35 @@ export const sharedChatRoutes = async (app) => {
|
||||
return reply.code(404).send({ success: false, error: { code: 'NOT_FOUND', message: 'Session not found' } })
|
||||
}
|
||||
const goodbyeSubmittedByMe = await hasUserSubmittedClosure(sessionId, request.userType)
|
||||
return reply.send({ success: true, data: { ...session, goodbye_submitted_by_me: goodbyeSubmittedByMe } })
|
||||
// Surface any pending extension so the mitra chat screen can recover the
|
||||
// _buildExtensionView state after a cold-start via FCM tap — without this,
|
||||
// the WS EXTENSION_REQUEST frame fired earlier has nothing to bind to.
|
||||
const [pendingExt] = await sql`
|
||||
SELECT id, requested_duration_minutes, requested_price, requested_at
|
||||
FROM session_extensions
|
||||
WHERE session_id = ${sessionId} AND status = 'pending'
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
let pending_extension = null
|
||||
if (pendingExt) {
|
||||
const { getExtensionTimeoutConfig } = await import('../../services/config.service.js')
|
||||
const { extension_timeout_seconds } = await getExtensionTimeoutConfig()
|
||||
const requestedAtMs = new Date(pendingExt.requested_at).getTime()
|
||||
const expiresAtMs = requestedAtMs + extension_timeout_seconds * 1000
|
||||
pending_extension = {
|
||||
extension_id: pendingExt.id,
|
||||
duration_minutes: pendingExt.requested_duration_minutes,
|
||||
price: pendingExt.requested_price,
|
||||
requested_at: pendingExt.requested_at,
|
||||
expires_at: new Date(expiresAtMs).toISOString(),
|
||||
timeout_seconds: extension_timeout_seconds,
|
||||
}
|
||||
}
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: { ...session, goodbye_submitted_by_me: goodbyeSubmittedByMe, pending_extension },
|
||||
})
|
||||
})
|
||||
|
||||
// Get full transcript (read-only, for history)
|
||||
|
||||
@@ -2,11 +2,15 @@ import { authenticate } from '../../plugins/auth.js'
|
||||
import { getSupportHandles } from '../../services/config.service.js'
|
||||
|
||||
/**
|
||||
* Phase 4 — Tanya Admin sheet handles. Sourced from `app_config.support_handles_json`,
|
||||
* Support channels (WA + Telegram). Sourced from `app_config.support_handles_json`,
|
||||
* editable by CC. Authenticated so unauthenticated callers can't enumerate the
|
||||
* support channels (rate-limit hardening, not a secret).
|
||||
* channels (rate-limit hardening, not a secret).
|
||||
*
|
||||
* Originally registered under /api/client (Phase 4 Tanya Admin sheet). Promoted
|
||||
* to /api/shared when the mitra Profil screen started linking the same WA/TG
|
||||
* contacts — same data, both audiences.
|
||||
*/
|
||||
export const clientSupportRoutes = async (app) => {
|
||||
export const sharedSupportRoutes = async (app) => {
|
||||
app.get('/support-handles', { preHandler: authenticate }, async (_request, reply) => {
|
||||
const handles = await getSupportHandles()
|
||||
return reply.send({ success: true, data: handles })
|
||||
@@ -128,18 +128,38 @@ export const getEarlyEndConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Phase 3.1: Mitra Ping Config ---
|
||||
// --- Mitra reachability config ---
|
||||
//
|
||||
// Two separate concerns, deliberately decoupled:
|
||||
// - heartbeat_cadence_seconds: how often the mitra app sends a heartbeat.
|
||||
// Fixed per backend deployment via the MITRA_HEARTBEAT_CADENCE_SECONDS
|
||||
// env (default 30). The mitra app reads this from /api/mitra/status and
|
||||
// uses it directly as its Timer.periodic interval.
|
||||
// - stale_after_seconds: how long the backend tolerates silence before
|
||||
// marking a mitra offline. DB-stored, CC-tunable. Must be >= the
|
||||
// heartbeat cadence (CC PATCH validates this).
|
||||
//
|
||||
// `require_ping` stays as the master switch — when false, the auto-offline
|
||||
// sweep is skipped entirely and mitras stay online forever once they toggle.
|
||||
|
||||
export const getMitraHeartbeatCadenceSeconds = () => {
|
||||
const raw = process.env.MITRA_HEARTBEAT_CADENCE_SECONDS
|
||||
if (!raw || raw.trim() === '') return 30
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
return Number.isFinite(parsed) && parsed >= 5 ? parsed : 30
|
||||
}
|
||||
|
||||
export const getMitraPingConfig = async () => {
|
||||
const [requireRow] = await sql`SELECT value FROM app_config WHERE key = 'require_mitra_ping'`
|
||||
const [intervalRow] = await sql`SELECT value FROM app_config WHERE key = 'mitra_ping_interval_seconds'`
|
||||
const [staleRow] = await sql`SELECT value FROM app_config WHERE key = 'mitra_stale_after_seconds'`
|
||||
return {
|
||||
require_ping: requireRow?.value?.value ?? true,
|
||||
ping_interval_seconds: intervalRow?.value?.value ?? 15,
|
||||
stale_after_seconds: staleRow?.value?.value ?? 45,
|
||||
heartbeat_cadence_seconds: getMitraHeartbeatCadenceSeconds(),
|
||||
}
|
||||
}
|
||||
|
||||
export const setMitraPingConfig = async ({ require_ping, ping_interval_seconds }) => {
|
||||
export const setMitraPingConfig = async ({ require_ping, stale_after_seconds }) => {
|
||||
if (require_ping !== undefined) {
|
||||
await sql`
|
||||
INSERT INTO app_config (key, value, updated_at)
|
||||
@@ -147,10 +167,10 @@ export const setMitraPingConfig = async ({ require_ping, ping_interval_seconds }
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
|
||||
`
|
||||
}
|
||||
if (ping_interval_seconds !== undefined) {
|
||||
if (stale_after_seconds !== undefined) {
|
||||
await sql`
|
||||
INSERT INTO app_config (key, value, updated_at)
|
||||
VALUES ('mitra_ping_interval_seconds', ${sql.json({ value: ping_interval_seconds })}, NOW())
|
||||
VALUES ('mitra_stale_after_seconds', ${sql.json({ value: stale_after_seconds })}, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
|
||||
`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { sendToSessionParticipant, isUserOnlineWs } from '../plugins/websocket.j
|
||||
import { extendSessionTimer, clearClosureGraceTimer, startClosureGraceTimer } from './session-timer.service.js'
|
||||
import { isMitraReachable } from './mitra-status.service.js'
|
||||
import { consumePaymentSession, failPaymentSession, getPaymentSession } from './payment.service.js'
|
||||
import { sendPushNotification } from './notification.service.js'
|
||||
import {
|
||||
getExtensionTimeoutConfig,
|
||||
getExtensionDefaultActionOnTimeout,
|
||||
@@ -48,11 +49,16 @@ const getExtensionTimeoutAction = async () => {
|
||||
* (mitra explicit accept OR auto-approve fires).
|
||||
*/
|
||||
export const requestExtension = async (sessionId, customerId, { duration_minutes, price, extension_payment_session_id }) => {
|
||||
// Verify session belongs to customer and is in an extendable state
|
||||
// Verify session belongs to customer and is in an extendable state.
|
||||
// customer_display_name is pulled along for the FCM body when the mitra
|
||||
// misses the WS frame.
|
||||
const [session] = await sql`
|
||||
SELECT id, customer_id, mitra_id, status, topic_sensitivity FROM chat_sessions
|
||||
WHERE id = ${sessionId} AND customer_id = ${customerId}
|
||||
AND status IN (${SessionStatus.ACTIVE}, ${SessionStatus.CLOSING})
|
||||
SELECT cs.id, cs.customer_id, cs.mitra_id, cs.status, cs.topic_sensitivity,
|
||||
c.display_name AS customer_display_name
|
||||
FROM chat_sessions cs
|
||||
INNER JOIN customers c ON c.id = cs.customer_id
|
||||
WHERE cs.id = ${sessionId} AND cs.customer_id = ${customerId}
|
||||
AND cs.status IN (${SessionStatus.ACTIVE}, ${SessionStatus.CLOSING})
|
||||
`
|
||||
if (!session) {
|
||||
throw Object.assign(new Error('Session not found or already ended'), { code: 'SESSION_NOT_ACTIVE', statusCode: 409 })
|
||||
@@ -103,8 +109,13 @@ export const requestExtension = async (sessionId, customerId, { duration_minutes
|
||||
const timeoutMs = await getExtensionTimeoutMs()
|
||||
const timeoutSeconds = Math.round(timeoutMs / 1000)
|
||||
|
||||
// Notify mitra — include current topic sensitivity so UI can highlight
|
||||
sendToSessionParticipant(sessionId, UserType.MITRA, {
|
||||
// Notify mitra — include current topic sensitivity so UI can highlight.
|
||||
// If the mitra isn't on this session's chat WS (on Home/Undangan, in
|
||||
// another chat, or app backgrounded), fall back to FCM. The session-
|
||||
// scoped WS is the only channel that reaches the in-chat `_buildExtensionView`
|
||||
// in real time; FCM gets them to /chat/session/:id, where chat connect
|
||||
// restores the pending extension state via /chat/:sessionId/info.
|
||||
const wsSent = sendToSessionParticipant(sessionId, UserType.MITRA, {
|
||||
type: WsMessage.EXTENSION_REQUEST,
|
||||
extension_id: extension.id,
|
||||
session_id: sessionId,
|
||||
@@ -114,6 +125,22 @@ export const requestExtension = async (sessionId, customerId, { duration_minutes
|
||||
timeout_seconds: timeoutSeconds,
|
||||
})
|
||||
|
||||
if (!wsSent) {
|
||||
await sendPushNotification(UserType.MITRA, session.mitra_id, {
|
||||
title: 'Permintaan Perpanjang',
|
||||
body: `${session.customer_display_name} mau lanjut +${duration_minutes} menit`,
|
||||
data: {
|
||||
type: WsMessage.EXTENSION_REQUEST,
|
||||
session_id: sessionId,
|
||||
extension_id: extension.id,
|
||||
duration_minutes,
|
||||
price,
|
||||
timeout_seconds: timeoutSeconds,
|
||||
action: 'open_extension',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Notify customer that chat is paused
|
||||
sendToSessionParticipant(sessionId, UserType.CUSTOMER, {
|
||||
type: WsMessage.SESSION_PAUSED,
|
||||
|
||||
@@ -96,7 +96,9 @@ export const getStatus = async (mitraId) => {
|
||||
return {
|
||||
...status,
|
||||
require_ping: pingConfig.require_ping,
|
||||
ping_interval_seconds: pingConfig.ping_interval_seconds,
|
||||
// The app reads this to set its Timer.periodic interval. Backend-fixed
|
||||
// (via env), not operator-tunable.
|
||||
heartbeat_cadence_seconds: pingConfig.heartbeat_cadence_seconds,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +136,12 @@ export const autoOfflineStaleMitras = async () => {
|
||||
// If ping is not required, skip the auto-offline sweep entirely
|
||||
if (!pingConfig.require_ping) return 0
|
||||
|
||||
const staleSeconds = pingConfig.ping_interval_seconds * 3
|
||||
// stale_after_seconds is the operator-facing knob — what they set is what
|
||||
// they get. No multiplier, no implicit "tolerate N missed heartbeats"
|
||||
// contract baked in. The CC PATCH validates that the value is >= the env-
|
||||
// driven heartbeat cadence so single missed pings can't flip a mitra
|
||||
// offline.
|
||||
const staleSeconds = pingConfig.stale_after_seconds
|
||||
const stale = await sql`
|
||||
UPDATE mitra_online_status
|
||||
SET is_online = false, last_offline_at = NOW(), updated_at = NOW()
|
||||
|
||||
@@ -33,7 +33,10 @@ export const sendPushNotification = async (recipientType, recipientId, { title,
|
||||
},
|
||||
android: {
|
||||
priority: 'high',
|
||||
notification: { channelId: 'chat_messages' },
|
||||
// Both apps register the same channel ID with the branded
|
||||
// notification sound (halobestie_notif.ogg in res/raw). See each
|
||||
// app's lib/core/notifications/notification_service.dart.
|
||||
notification: { channelId: 'halobestie_chat_v1' },
|
||||
},
|
||||
apns: {
|
||||
payload: {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
env:
|
||||
# App identifiers — Android / iOS bundle IDs picked up automatically by `appId:` in flows.
|
||||
APP_ID_ANDROID: com.halobestie.client.client_app
|
||||
APP_ID_IOS: com.halobestie.client.clientApp
|
||||
APP_ID_ANDROID: com.mybestie
|
||||
APP_ID_IOS: com.mybestie
|
||||
|
||||
# Backend the app talks to — must match what the installed APK was built with
|
||||
# (the `--dart-define=API_BASE_URL=...` value at build time).
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# Pre-req: client_app debug APK installed, backend reachable at
|
||||
# BACKEND_URL/BACKEND_INTERNAL_URL, NODE_ENV != 'production' (so the
|
||||
# /internal/_test/peek-otp + /internal/_test/reset-phone routes register).
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+628155556677"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# NOTE: numeric prefix conflicts with the existing
|
||||
# 02_cta_disabled_when_no_mitra.yaml — Stage 9 will reorganize the flow
|
||||
# directory once the full Phase 4 suite lands.
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+628155557701"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#
|
||||
# NOTE: numeric prefix conflicts with the existing 03_payment_to_chat_happy.yaml
|
||||
# — Stage 9 will reorganize the flow directory once the full Phase 4 suite lands.
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
---
|
||||
- launchApp:
|
||||
clearState: true
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/04_payment_expired.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+628155557704"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# mitra is force-timed-out server-side regardless of availability.
|
||||
# 2. anonymity_enabled=true on the dev backend.
|
||||
# 3. NODE_ENV != 'production' (so /internal/_test/* routes register).
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+628155557705"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/08_returning_targeted.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+628155556677"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/09_chat_tab.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+628155556678"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
# simply suppresses the notification); the banner is the only
|
||||
# user-visible signal that they're missing alerts, which is what we
|
||||
# assert below.
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# - Backend reachable; NODE_ENV != 'production'.
|
||||
# - ≥1 mitra online (mitraAvailable gates the "aku mau curhat" CTA).
|
||||
# - first_session_discount is enabled in the pricing config (default).
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# 3. has_transacted is implicitly false (no chat_sessions →
|
||||
# isCustomerEligibleForFirstSessionDiscount returns true).
|
||||
# 4. /payment/entry routes to /payment/discount-paywall (S6).
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# Pre-reqs:
|
||||
# - ≥1 mitra online (seed_history_session pairs with the most-recent
|
||||
# online mitra to write the chat_sessions row).
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# This flow inlines the pre-OTP onboarding steps (instead of using the
|
||||
# onboarding_new_user_verified subflow) because we want to enter wrong OTPs
|
||||
# rather than the peeked valid one.
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# stays AuthAnonymousData). USP screen pushes /payment/method-pick
|
||||
# directly when verified=false. Verifies onboardingIntent is NOT set
|
||||
# (it stays `recover` because we picked "curhat anonim", not "verifikasi").
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-customer-02-10-recover_via_masuk_existing_user_to_home.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
EXISTING_NAME: "Returning User"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-01_returning_lama_online.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-02_returning_lama_offline_blast.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-03_returning_lama_offline_tanya_admin.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-04_returning_baru_blast.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-05_payment_expired_retry_preserves_targeting.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
#
|
||||
# Run:
|
||||
# maestro test client_app/.maestro/flows/ts-06_targeted_reject_fallback_to_blast.yaml
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
# - Backend reachable; NODE_ENV != 'production'.
|
||||
# - ≥1 mitra online (the seeded mitra acts as the blast acceptor and the
|
||||
# subsequent message sender).
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
# - The currently-signed-in mitra must have a `fcm_token` row in
|
||||
# `mitras.fcm_token`; otherwise the FCM dispatch succeeds at the
|
||||
# backend code path but never reaches a device.
|
||||
appId: com.halobestie.client.client_app
|
||||
appId: com.mybestie
|
||||
env:
|
||||
TEST_PHONE: "+6281234567890"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
|
||||
@@ -9,7 +9,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.halobestie.client.client_app"
|
||||
namespace = "com.mybestie"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@@ -25,7 +25,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.halobestie.client.client_app"
|
||||
applicationId = "com.mybestie"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 24
|
||||
|
||||
38
client_app/android/app/google-services.json
Normal file → Executable file
@@ -23,6 +23,44 @@
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1068156046511:android:f30784f6b0423131b8185a",
|
||||
"android_client_info": {
|
||||
"package_name": "com.halobestie.mitra"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1068156046511:android:4f8fe9a3c7c14c57b8185a",
|
||||
"android_client_info": {
|
||||
"package_name": "com.mybestie"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Phase 4 Stage 4 notif-gate via permission_handler. -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<application
|
||||
android:label="client_app"
|
||||
android:label="HaloBestie"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.halobestie.mitra.mitra_app
|
||||
package com.mybestie
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1000 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
client_app/android/app/src/main/res/raw/halobestie_notif.ogg
Normal file
4
client_app/android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FF699F</color>
|
||||
</resources>
|
||||
BIN
client_app/assets/icons/logo.png
Executable file
|
After Width: | Height: | Size: 275 KiB |
@@ -496,7 +496,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.halobestie.client.clientApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -513,7 +513,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.halobestie.client.clientApp.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -531,7 +531,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.halobestie.client.clientApp.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -547,7 +547,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.halobestie.client.clientApp.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -558,7 +558,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -615,7 +615,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -679,7 +679,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.halobestie.client.clientApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -702,7 +702,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.halobestie.client.clientApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 86 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 114 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 97 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 144 B |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 114 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 342 B |
|
After Width: | Height: | Size: 133 B |
|
After Width: | Height: | Size: 289 B |
|
After Width: | Height: | Size: 145 B |
|
After Width: | Height: | Size: 330 B |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 342 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 493 B |
|
After Width: | Height: | Size: 166 B |
|
After Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 169 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 600 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 525 B |
4
client_app/ios/Runner/GoogleService-Info.plist
Normal file → Executable file
@@ -9,7 +9,7 @@
|
||||
<key>PLIST_VERSION</key>
|
||||
<string>1</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>com.halobestie.client.clientApp</string>
|
||||
<string>com.mybestie</string>
|
||||
<key>PROJECT_ID</key>
|
||||
<string>halobestie-clone-dev</string>
|
||||
<key>STORAGE_BUCKET</key>
|
||||
@@ -25,6 +25,6 @@
|
||||
<key>IS_SIGNIN_ENABLED</key>
|
||||
<true></true>
|
||||
<key>GOOGLE_APP_ID</key>
|
||||
<string>1:1068156046511:ios:c7786cedb9101d34b8185a</string>
|
||||
<string>1:1068156046511:ios:498ab71cbbbd6822b8185a</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -9,7 +9,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Client App</string>
|
||||
<string>HaloBestie</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>client_app</string>
|
||||
<string>HaloBestie</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -8,11 +8,17 @@ class NotificationService {
|
||||
static final _localNotifications = FlutterLocalNotificationsPlugin();
|
||||
static GoRouter? _router;
|
||||
|
||||
// Channel ID bumped (`chat_messages` → `halobestie_chat_v1`) when the
|
||||
// branded notification sound was introduced. Android binds sound to a
|
||||
// channel at create time on API 26+, so an existing channel can't pick
|
||||
// up a new sound — a fresh ID is the only way. Backend FCM payloads
|
||||
// target the same ID — see backend/src/services/notification.service.js.
|
||||
static const _channel = AndroidNotificationChannel(
|
||||
'chat_messages',
|
||||
'Chat Messages',
|
||||
description: 'Notifications for incoming chat messages',
|
||||
'halobestie_chat_v1',
|
||||
'Chat HaloBestie',
|
||||
description: 'Notifications for incoming chat messages and pairing requests',
|
||||
importance: Importance.high,
|
||||
sound: RawResourceAndroidNotificationSound('halobestie_notif'),
|
||||
);
|
||||
|
||||
static Future<void> initialize(GoRouter router) async {
|
||||
@@ -60,6 +66,9 @@ class NotificationService {
|
||||
channelDescription: _channel.description,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
// API 26+ ignores this in favor of the channel's sound; included
|
||||
// for the API 24/25 path where channels don't exist yet.
|
||||
sound: const RawResourceAndroidNotificationSound('halobestie_notif'),
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
|
||||
@@ -22,6 +22,9 @@ class HaloTokens {
|
||||
static const Color brandDark = Color(0xFF8C3255);
|
||||
static const Color brandSoft = Color(0xFFF7E4E9);
|
||||
static const Color brandSofter = Color(0xFFFBEFF3);
|
||||
// Launcher-icon background. Use this pink behind monochrome/white logos.
|
||||
// For full-color logos, use `surface` (#FFFFFF) as the icon background.
|
||||
static const Color brandLogoBg = Color(0xFFFF699F);
|
||||
static const Color accent = Color(0xFFF7B26A);
|
||||
static const Color accentSoft = Color(0xFFFCEAD3);
|
||||
static const Color mint = Color(0xFFB8DBC8);
|
||||
|
||||
@@ -27,7 +27,7 @@ class SupportHandles {
|
||||
|
||||
final supportHandlesProvider = FutureProvider<SupportHandles>((ref) async {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final response = await api.get('/api/client/support-handles');
|
||||
final response = await api.get('/api/shared/support-handles');
|
||||
final data = response['data'] as Map<String, dynamic>? ?? const {};
|
||||
return SupportHandles.fromJson(data);
|
||||
});
|
||||
|
||||
@@ -63,6 +63,6 @@ class DefaultFirebaseOptions {
|
||||
messagingSenderId: '1068156046511',
|
||||
projectId: 'halobestie-clone-dev',
|
||||
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
||||
iosBundleId: 'com.halobestie.client.clientApp',
|
||||
iosBundleId: 'com.mybestie',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.4"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -358,6 +366,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.5"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -607,6 +623,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -879,6 +903,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -58,12 +58,29 @@ dev_dependencies:
|
||||
build_runner: ^2.4.13
|
||||
custom_lint: ^0.7.0
|
||||
riverpod_lint: ^2.6.2
|
||||
# Generates launcher icons for Android + iOS from a single source PNG.
|
||||
# Config block below; run `dart run flutter_launcher_icons` to regenerate.
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
# In-repo lint rules — lives at the repo root so client_app + mitra_app
|
||||
# share the same set. Adds `no_ref_in_dispose` and any future repo-wide
|
||||
# guardrails. See halo_lints/lib/halo_lints.dart.
|
||||
halo_lints:
|
||||
path: ../halo_lints
|
||||
|
||||
# Launcher-icon config. Background color mirrors HaloTokens.brandLogoBg
|
||||
# (#FF699F) from lib/core/theme/halo_tokens.dart — the documented bg for
|
||||
# monochrome/white logos. If the source logo ever becomes full-color,
|
||||
# switch both values to `#FFFFFF` (HaloTokens.surface).
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/icons/logo.png"
|
||||
remove_alpha_ios: true
|
||||
background_color_ios: "#FF699F"
|
||||
min_sdk_android: 24
|
||||
adaptive_icon_background: "#FF699F"
|
||||
adaptive_icon_foreground: "assets/icons/logo.png"
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
|
||||
@@ -452,7 +452,7 @@ export default function SettingsPage() {
|
||||
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<h2>Mitra Online Status (Ping)</h2>
|
||||
<p>Konfigurasi apakah mitra harus mengirim ping (heartbeat) untuk tetap online.</p>
|
||||
<p>Mitra dianggap online selama heartbeat terakhir berusia ≤ ambang batas. Cadence (frekuensi ping aplikasi) di-fix oleh server lewat env var.</p>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -465,21 +465,27 @@ export default function SettingsPage() {
|
||||
<p style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>
|
||||
Jika dinonaktifkan, mitra akan tetap online tanpa perlu mengirim ping. QC bertanggung jawab atas kualitas layanan mitra.
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<label>Interval Ping:</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
<label>Ambang offline (heartbeat terakhir lebih lama dari):</label>
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
value={mpData?.ping_interval_seconds ?? 15}
|
||||
min={mpData?.heartbeat_cadence_seconds ?? 30}
|
||||
value={mpData?.stale_after_seconds ?? 45}
|
||||
onChange={e => {
|
||||
const val = parseInt(e.target.value, 10)
|
||||
if (val >= 5) mpMutation.mutate({ ping_interval_seconds: val })
|
||||
const floor = mpData?.heartbeat_cadence_seconds ?? 30
|
||||
if (Number.isFinite(val) && val >= floor) {
|
||||
mpMutation.mutate({ stale_after_seconds: val })
|
||||
}
|
||||
}}
|
||||
disabled={mpMutation.isPending}
|
||||
style={{ width: 80 }}
|
||||
/>
|
||||
<span>detik</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: '#666', marginBottom: 0 }}>
|
||||
Cadence ping mitra: <strong>{mpData?.heartbeat_cadence_seconds ?? 30} detik</strong> (server-set via MITRA_HEARTBEAT_CADENCE_SECONDS env). Nilai ambang minimum mengikuti cadence — tidak bisa lebih rendah.
|
||||
</p>
|
||||
{mpMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
env:
|
||||
# App identifiers
|
||||
APP_ID_ANDROID: com.halobestie.mitra.mitra_app
|
||||
APP_ID_IOS: com.halobestie.mitra
|
||||
APP_ID_ANDROID: com.mybestie.mitra
|
||||
APP_ID_IOS: com.mybestie.mitra
|
||||
|
||||
# Backend the app talks to — must match what the installed APK was built with.
|
||||
BACKEND_URL: http://192.168.88.247:3000
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
# Run:
|
||||
# maestro test mitra_app/.maestro/flows/01_smoke.yaml
|
||||
#
|
||||
# Pre-req: mitra_app debug APK installed on the connected device, signed in as a mitra.
|
||||
# Pre-req: mitra_app debug APK installed on the connected device, signed in
|
||||
# as a mitra. Stage 2 removed the "Sesi Aktif" / "Riwayat Chat" tiles — the
|
||||
# stable marker on the new BestieHome is the status card "Kamu lagi
|
||||
# ONLINE" / "Kamu lagi OFFLINE". Either is fine for the smoke check.
|
||||
appId: ${APP_ID_ANDROID}
|
||||
---
|
||||
- launchApp:
|
||||
clearState: false
|
||||
- assertVisible:
|
||||
text: "Sesi Aktif|Riwayat Chat"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 10000
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
# Verifies the online/offline toggle works and reflects in the UI.
|
||||
# This is independent of the customer side — pure mitra UI test.
|
||||
#
|
||||
# Stage 2 replaced the Switch widget with a single "Ganti Status" CTA on
|
||||
# the online variant (and "Nyalain Status (Online)" on the offline variant).
|
||||
# The status card copy ("Kamu lagi ONLINE" / "Kamu lagi OFFLINE") is the
|
||||
# stable marker for the current state.
|
||||
#
|
||||
# A more thorough version of this test is now in
|
||||
# ts-mitra-1-03-toggle_online_to_offline.yaml — that one walks the full
|
||||
# auth flow and screenshots both states. This file keeps the lightweight
|
||||
# variant for fast smoke iteration on a pre-signed-in device.
|
||||
#
|
||||
# Run:
|
||||
# maestro test mitra_app/.maestro/flows/02_online_offline_toggle.yaml
|
||||
appId: ${APP_ID_ANDROID}
|
||||
@@ -8,16 +18,17 @@ appId: ${APP_ID_ANDROID}
|
||||
- launchApp:
|
||||
clearState: false
|
||||
|
||||
# Find the toggle and capture initial state.
|
||||
# Establish baseline — exactly one of the two status-card labels is up.
|
||||
- assertVisible:
|
||||
text: "Online|Offline"
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
|
||||
# Tap the toggle — it's a Switch widget; Maestro can tap by adjacent text label.
|
||||
- tapOn:
|
||||
text: "Online|Offline"
|
||||
# Tap whichever CTA is currently rendered. Online → "Ganti Status".
|
||||
# Offline → "Nyalain Status (Online)". The regex matches both.
|
||||
- tapOn: "(?s).*(Ganti Status|Nyalain Status \\(Online\\)).*"
|
||||
|
||||
# After flipping, the opposite label should appear within ~2s
|
||||
# (status is server-confirmed via /api/mitra/status/online or /offline).
|
||||
- assertVisible:
|
||||
text: "Online|Offline"
|
||||
# After the POST /api/mitra/status/{online,offline} response lands, the
|
||||
# opposite status label should be visible within ~2s.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 5000
|
||||
|
||||
@@ -8,26 +8,41 @@
|
||||
# 3. The customer has an existing confirmed payment_session ready to blast (use the
|
||||
# seed_customer_pending_blast.sh helper)
|
||||
#
|
||||
# A more thorough version that walks auth + asserts every popup element is in
|
||||
# ts-mitra-3-01-incoming_popup_curhat_baru.yaml; this file keeps the
|
||||
# lightweight smoke version for fast iteration on a pre-signed-in device.
|
||||
#
|
||||
# Run:
|
||||
# maestro test mitra_app/.maestro/flows/03_accept_general_blast.yaml
|
||||
appId: ${APP_ID_ANDROID}
|
||||
---
|
||||
- launchApp:
|
||||
clearState: false
|
||||
- assertVisible: "Online" # ensure mitra is online before triggering the blast
|
||||
|
||||
# Ensure mitra is online before triggering the blast. Stage 2 swapped the
|
||||
# Switch widget for a "Kamu lagi ONLINE" status card.
|
||||
- assertVisible:
|
||||
text: "(?s).*Kamu lagi ONLINE.*"
|
||||
|
||||
# Step 1: simulate a customer creating a confirmed payment + firing a general blast.
|
||||
# This script returns once the blast notification has been sent to this mitra.
|
||||
- runScript: ../scripts/customer_blast_now.sh
|
||||
|
||||
# Step 2: incoming-request overlay appears on this device
|
||||
- assertVisible:
|
||||
text: "Terima"
|
||||
# Step 2: incoming-request popup appears on this device (BestieIncomingPopup,
|
||||
# variant=new — pink-bordered card with "Curhat Baru!" headline).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Curhat Baru!.*"
|
||||
timeout: 10000
|
||||
- assertVisible: "Tolak"
|
||||
|
||||
# Step 3: mitra accepts → overlay closes, chat opens
|
||||
- tapOn: "Terima"
|
||||
- assertVisible:
|
||||
text: "Sesi Aktif"
|
||||
timeout: 5000
|
||||
text: "(?s).*Terima.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Tolak.*"
|
||||
|
||||
# Step 3: mitra accepts → popup closes, chat opens. BestieChatV5 active
|
||||
# subtitle is "sesi aktif · Chat".
|
||||
- tapOn: "(?s).*Terima.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*sesi aktif · Chat.*"
|
||||
timeout: 10000
|
||||
|
||||
@@ -12,7 +12,7 @@ Tests use the naming convention `ts-mitra-<section>-<sub>-<description>.yaml`:
|
||||
| File | Branch (spec ref) | Expected destination |
|
||||
|---|---|---|
|
||||
| `ts-mitra-A-01-phone_invalid_inline_error.yaml` | §A.1 local CTA gate for short phones | "kirim kode" disabled for <9 subscriber digits, enables + navigates on valid input |
|
||||
| `ts-mitra-A-03-success_login_to_home.yaml` | §A.2 200 + `is_active=true` | `/home` (active sessions tab) |
|
||||
| `ts-mitra-A-03-success_login_to_home.yaml` | §A.2 200 + `is_active=true` | `/home` (BestieHome online; asserts "Kamu lagi ONLINE" — Stage 2 removed the Sesi Aktif / Riwayat Chat tiles) |
|
||||
| `ts-mitra-A-04-account_inactive_screen.yaml` | §A.2 200 + `is_active=false` → 403 `ACCOUNT_INACTIVE` | `/auth/inactive` (AppBar back arrow omitted; system-back interception deferred) |
|
||||
| `ts-mitra-A-05-phone_format_variants.yaml` | §A.1 phone-format normalization | 5 variants (`8…`, `08…`, `628…`, `+628…`, `0628…`) all reach S3b with `+628200000501` shown |
|
||||
| `ts-mitra-A-06-back_to_login_and_retry.yaml` | §A.1 regression — back from S3b + re-submit | Second "kirim kode" tap still navigates to S3b after returning to S3a |
|
||||
@@ -39,6 +39,10 @@ interference:
|
||||
- A-04 → `+628200000401`
|
||||
- A-05 → `+628200000501` (one phone, 5 input formats)
|
||||
- A-06 → `+628200000601`
|
||||
- §1 Home (ts-mitra-1-*) → `+62820000070{1..3}`
|
||||
- §2 Undangan (ts-mitra-2-*) → `+62820000080{1..2}` (2-03 piggybacks on a
|
||||
pre-signed-in device, no fresh OTP)
|
||||
- §3 Popup + Chat (ts-mitra-3-*) → `+62820000090{1..4}`
|
||||
|
||||
If the same phone gets used across multiple flows in one run, the per-IP
|
||||
rate-limit (10 OTP requests / hour, default) can trip and break A-03 or A-04
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# ts-mitra-1-01 — §1 Bestie Home (online variant) renders end-to-end
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1 + figma BestieHome (v4.jsx:417)
|
||||
#
|
||||
# Walks: seed active mitra → reset OTP → S3a → S3b → /home → assert online
|
||||
# variant chrome (greeting, tiles, status card, Ganti Status CTA, Pengingat,
|
||||
# BestieTabBar). Screenshot at the end so this also serves as a design-review
|
||||
# evidence baseline for Stage 7.
|
||||
#
|
||||
# A successful login lands on /home with the mitra already ONLINE — the
|
||||
# status_notifier's load() seeds StatusLoadedData(isOnline:true) for any
|
||||
# mitra that's been online in this dev DB, and the maestro test mitras are
|
||||
# left online by prior runs. To make this test deterministic regardless of
|
||||
# prior state, we reset-all-mitras-online before launching.
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000701"
|
||||
MITRA_DISPLAY_NAME: "Maestro Home Online"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 10000
|
||||
|
||||
# S3a — request OTP.
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000701"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
|
||||
# Peek + submit correct code.
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# Home renders — wait for the greeting then assert the rest of the chrome.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Bestie Maestro Home Online.*"
|
||||
timeout: 15000
|
||||
- assertVisible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Undangan.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Perpanjang.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*(Ganti Status|Nyalain Status).*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Pengingat.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Opening protocol.*"
|
||||
# BestieTabBar: Home / Chat / Profil
|
||||
- assertVisible:
|
||||
text: "(?s).*Home.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Chat.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Profil.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-01-home-online
|
||||
@@ -0,0 +1,77 @@
|
||||
# ts-mitra-1-01a — §1 Home after login, SCENARIO 1: freshly created mitra
|
||||
# with NO mitra_online_status row → home renders OFFLINE.
|
||||
#
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1
|
||||
# DB invariant: mitras row exists; mitra_online_status row absent.
|
||||
# Expected behavior: app's first GET /api/mitra/status creates a row via
|
||||
# ensureStatusRow() with DB default is_online=false → BestieHomeOffline.
|
||||
#
|
||||
# Setup: seed_mitra creates the mitra row. delete_mitra_status_row removes
|
||||
# any pre-existing online_status row so this run starts from the true
|
||||
# "freshly created" state (even if a prior test run had touched the row).
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000711"
|
||||
MITRA_DISPLAY_NAME: "Maestro Fresh User"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
# Remove any pre-existing mitra_online_status row so the precondition is
|
||||
# precisely "freshly created mitra, no status row". The app's status call
|
||||
# will recreate the row with default is_online=false.
|
||||
- runScript:
|
||||
file: ../scripts/delete_mitra_status_row.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 15000
|
||||
|
||||
# S3a → S3b → /home
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000711"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# Verify: fresh user lands on BestieHomeOffline.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Bestie Maestro Fresh User.*"
|
||||
timeout: 15000
|
||||
- assertVisible:
|
||||
text: "(?s).*Kamu lagi OFFLINE.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*🌙.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Nyalain Status \\(Online\\).*"
|
||||
# Negative: should NOT render the online variant chrome.
|
||||
- assertNotVisible: "(?s).*Kamu lagi ONLINE.*"
|
||||
- assertNotVisible: "(?s).*Pengingat.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-01a-fresh-user-offline
|
||||
@@ -0,0 +1,77 @@
|
||||
# ts-mitra-1-01b — §1 Home after login, SCENARIO 2: existing mitra who was
|
||||
# OFFLINE before logout → relogin shows OFFLINE.
|
||||
#
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1
|
||||
# DB invariant: mitras row exists; mitra_online_status row exists with
|
||||
# is_online=false (the post-logout state of someone who toggled offline
|
||||
# before signing out).
|
||||
# Expected behavior: app's GET /api/mitra/status returns is_online=false →
|
||||
# BestieHomeOffline.
|
||||
#
|
||||
# Setup: seed_mitra + force_mitra_offline simulates the post-logout state
|
||||
# of an existing user who was offline.
|
||||
#
|
||||
# This is functionally identical to ts-mitra-1-02 but tracks the SCENARIO 2
|
||||
# slot explicitly. Different phone slot so it doesn't collide.
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000712"
|
||||
MITRA_DISPLAY_NAME: "Maestro Existing Offline"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
# Force OFFLINE — simulates someone who toggled off before logout.
|
||||
- runScript:
|
||||
file: ../scripts/force_mitra_offline.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 15000
|
||||
|
||||
# S3a → S3b → /home
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000712"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# Verify: relogin lands on BestieHomeOffline (still offline from pre-logout).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Bestie Maestro Existing Offline.*"
|
||||
timeout: 15000
|
||||
- assertVisible:
|
||||
text: "(?s).*Kamu lagi OFFLINE.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*🌙.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Nyalain Status \\(Online\\).*"
|
||||
- assertNotVisible: "(?s).*Kamu lagi ONLINE.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-01b-existing-offline-relogin
|
||||
@@ -0,0 +1,82 @@
|
||||
# ts-mitra-1-01c — §1 Home after login, SCENARIO 3: existing mitra who was
|
||||
# ONLINE before logout → relogin shows ONLINE.
|
||||
#
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1
|
||||
# DB invariant: mitras row exists; mitra_online_status row exists with
|
||||
# is_online=true (the post-logout state of someone who stayed online before
|
||||
# signing out, or whose ONLINE state was preserved across sessions).
|
||||
# Expected behavior: app's GET /api/mitra/status returns is_online=true →
|
||||
# BestieHome (online variant): 🌸 greeting, tile grid, ONLINE status card,
|
||||
# Ganti Status CTA, Pengingat.
|
||||
#
|
||||
# Setup: seed_mitra + force_mitra_online makes the existing user ONLINE,
|
||||
# then login should reflect that state.
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000713"
|
||||
MITRA_DISPLAY_NAME: "Maestro Existing Online"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
# Force ONLINE — simulates someone who was online at logout time.
|
||||
- runScript:
|
||||
file: ../scripts/force_mitra_online.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 15000
|
||||
|
||||
# S3a → S3b → /home
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000713"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# Verify: relogin lands on BestieHome (online variant — still online from pre-logout).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Bestie Maestro Existing Online.*"
|
||||
timeout: 15000
|
||||
- assertVisible:
|
||||
text: "(?s).*Kamu lagi ONLINE.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*🌸.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Ganti Status.*"
|
||||
# Online variant has the tile grid + Pengingat.
|
||||
- assertVisible:
|
||||
text: "(?s).*Undangan.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Perpanjang.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Pengingat.*"
|
||||
- assertNotVisible: "(?s).*Kamu lagi OFFLINE.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-01c-existing-online-relogin
|
||||
@@ -0,0 +1,74 @@
|
||||
# ts-mitra-1-02 — §1 Bestie Home (offline variant) renders
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1 + figma BestieHomeOffline (v5.jsx:188)
|
||||
#
|
||||
# Same auth as 1-01 but the mitra is forced OFFLINE in the DB before the
|
||||
# app launches. The status_notifier's GET /api/mitra/status returns
|
||||
# is_online=false on load → the home renders the offline variant: 🌙
|
||||
# greeting, 😴 OFFLINE card, "Nyalain Status (Online)" CTA, and crucially
|
||||
# NO tiles / NO Pengingat (those are online-only chrome).
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000702"
|
||||
MITRA_DISPLAY_NAME: "Maestro Home Offline"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
# Force this mitra OFFLINE so the GET /api/mitra/status that fires on home
|
||||
# mount returns is_online=false. seed_mitra.js exposed MITRA_ID for us.
|
||||
- runScript:
|
||||
file: ../scripts/force_mitra_offline.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 10000
|
||||
|
||||
# S3a → S3b → /home
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000702"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# Offline variant chrome
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Bestie Maestro Home Offline.*"
|
||||
timeout: 15000
|
||||
# The header greeting suffix flips to 🌙 in the offline variant.
|
||||
- assertVisible:
|
||||
text: "(?s).*🌙.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Kamu lagi OFFLINE.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Nyalain Status \\(Online\\).*"
|
||||
# Negative assertions: tiles and Pengingat are online-only chrome.
|
||||
- assertNotVisible: "(?s).*Pengingat.*"
|
||||
- assertNotVisible: "(?s).*Opening protocol.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-02-home-offline
|
||||
@@ -0,0 +1,77 @@
|
||||
# ts-mitra-1-03 — §1 Ganti Status toggles online ⇄ offline UI
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1
|
||||
#
|
||||
# Starts on /home in the online variant, taps "Ganti Status" → asserts the
|
||||
# offline variant takes over, taps "Nyalain Status (Online)" → asserts the
|
||||
# online variant returns. Screenshots at both states for the design review.
|
||||
#
|
||||
# Online toggle posts /api/mitra/status/online (offline → /offline). The
|
||||
# status_notifier sets StatusLoadedData immediately on success so the UI
|
||||
# flips within ~1 frame after the response.
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000703"
|
||||
MITRA_DISPLAY_NAME: "Maestro Toggle"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 10000
|
||||
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000703"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# Online variant on first land.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 15000
|
||||
- takeScreenshot: ts-mitra-1-03-online-before-toggle
|
||||
|
||||
# Tap Ganti Status → flip to offline.
|
||||
- tapOn: "(?s).*(Ganti Status|Nyalain Status).*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi OFFLINE.*"
|
||||
timeout: 10000
|
||||
- assertVisible:
|
||||
text: "(?s).*Nyalain Status \\(Online\\).*"
|
||||
- takeScreenshot: ts-mitra-1-03-offline-after-toggle
|
||||
|
||||
# Tap Nyalain Status → flip back to online.
|
||||
- tapOn: "(?s).*Nyalain Status \\(Online\\).*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 10000
|
||||
- assertVisible:
|
||||
text: "(?s).*(Ganti Status|Nyalain Status).*"
|
||||
- takeScreenshot: ts-mitra-1-03-online-after-second-toggle
|
||||
@@ -0,0 +1,92 @@
|
||||
# ts-mitra-1-04 — §1 Home Undangan tile → Chat tab (Curhat Baru sub-tab)
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1
|
||||
#
|
||||
# The Undangan tile on BestieHome (home_screen.dart L200-212) writes 0 to
|
||||
# undanganTabProvider then calls shell.goBranch(1) which routes to the Chat
|
||||
# branch (Undangan). UndanganScreen reads the provider on init and selects
|
||||
# the Curhat Baru tab (index 0).
|
||||
#
|
||||
# Walks: login → home online → tap Undangan tile → assert we landed on the
|
||||
# Chat branch with Curhat Baru as the active sub-tab (empty-state copy
|
||||
# proves we're on the right sub-tab).
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000704"
|
||||
MITRA_DISPLAY_NAME: "Maestro Undangan Tile"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 10000
|
||||
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000704"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 15000
|
||||
|
||||
# Conditional online toggle. Fresh mitra defaults to OFFLINE (per DB schema
|
||||
# default for mitra_online_status.is_online). If a prior test run force-onlined
|
||||
# this row already, skip the CTA. Otherwise tap "Nyalain Status (Online)" so
|
||||
# downstream flow has the tile grid + is blast-eligible.
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi OFFLINE.*"
|
||||
commands:
|
||||
- tapOn: "(?s).*Nyalain Status.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi ONLINE.*"
|
||||
timeout: 10000
|
||||
|
||||
# Tap the Undangan tile (online-only chrome — tile grid is hidden when offline).
|
||||
# Use the tile label "Undangan" rather than the icon (emoji selectors are flaky
|
||||
# across renderers). The tile is the topmost match for "Undangan" since the
|
||||
# tab bar label hasn't rendered yet on the Home screen.
|
||||
- tapOn: "(?s).*Undangan.*"
|
||||
|
||||
# Curhat Baru sub-tab visible + empty-state copy (no pending invites in this
|
||||
# fresh DB row). Both tabs labels are visible — verify Curhat Baru is the
|
||||
# active one by asserting on the empty-state copy (which only appears under
|
||||
# the Curhat Baru tab, not under Perpanjang).
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Curhat Baru.*"
|
||||
timeout: 8000
|
||||
- assertVisible:
|
||||
text: "(?s).*Perpanjang Curhat.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Belum ada undangan masuk.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-04-curhat-baru-from-tile
|
||||
@@ -0,0 +1,84 @@
|
||||
# ts-mitra-1-05 — §1 Home Perpanjang tile → Chat tab (Perpanjang sub-tab)
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1
|
||||
#
|
||||
# The Perpanjang tile (home_screen.dart L220-231) writes 1 to
|
||||
# undanganTabProvider then calls shell.goBranch(1). UndanganScreen picks
|
||||
# Perpanjang Curhat (index 1) on init.
|
||||
#
|
||||
# Walks: login → home online → tap Perpanjang tile → assert Perpanjang sub-
|
||||
# tab is active (empty-state placeholder copy from _PerpanjangTab visible).
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000705"
|
||||
MITRA_DISPLAY_NAME: "Maestro Perpanjang Tile"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 10000
|
||||
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000705"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 15000
|
||||
|
||||
# Conditional online toggle. Fresh mitra defaults to OFFLINE (per DB schema
|
||||
# default for mitra_online_status.is_online). If a prior test run force-onlined
|
||||
# this row already, skip the CTA. Otherwise tap "Nyalain Status (Online)" so
|
||||
# downstream flow has the tile grid + is blast-eligible.
|
||||
- runFlow:
|
||||
when:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi OFFLINE.*"
|
||||
commands:
|
||||
- tapOn: "(?s).*Nyalain Status.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi ONLINE.*"
|
||||
timeout: 10000
|
||||
|
||||
# Tap the Perpanjang tile.
|
||||
- tapOn: "(?s).*Perpanjang.*"
|
||||
|
||||
# Perpanjang sub-tab active → placeholder empty-state copy visible.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Belum ada permintaan perpanjangan.*"
|
||||
timeout: 8000
|
||||
- assertVisible:
|
||||
text: "(?s).*Curhat Baru.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Perpanjang Curhat.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-05-perpanjang-from-tile
|
||||
@@ -0,0 +1,77 @@
|
||||
# ts-mitra-1-06 — §1 Offline variant: tile grid (Undangan/Perpanjang) is hidden
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §1 (offline branch)
|
||||
#
|
||||
# When the mitra is OFFLINE the home renders BestieHomeOffline which (per
|
||||
# Stage 2) drops the entire tile grid + Pengingat — only the greeting,
|
||||
# status card, and "Nyalain Status (Online)" CTA remain.
|
||||
#
|
||||
# Negative coverage: tile labels "Undangan" / "Perpanjang" are NOT visible
|
||||
# on Home in the offline state. Complements ts-mitra-1-02 (which is
|
||||
# focused on the offline chrome) with a tile-grid-specific negative assertion.
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000706"
|
||||
MITRA_DISPLAY_NAME: "Maestro Tiles Hidden"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
# Force this mitra OFFLINE so the GET /api/mitra/status on home mount
|
||||
# returns is_online=false → BestieHomeOffline variant renders without the
|
||||
# tile grid.
|
||||
- runScript:
|
||||
file: ../scripts/force_mitra_offline.js
|
||||
env:
|
||||
MITRA_ID: ${output.MITRA_ID}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 30000
|
||||
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000706"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
# Offline variant lands.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi OFFLINE.*"
|
||||
timeout: 15000
|
||||
- assertVisible:
|
||||
text: "(?s).*Nyalain Status \\(Online\\).*"
|
||||
|
||||
# Tile-grid labels MUST NOT be visible — _PrimaryTileRow is not rendered.
|
||||
# Note: the "Chat" tab label in the BestieTabBar is still visible at the
|
||||
# bottom; the negative assertions target the tile-specific labels
|
||||
# "Undangan" and "Perpanjang" which only appear in the tile-grid widgets.
|
||||
- assertNotVisible: "(?s).*Undangan.*"
|
||||
- assertNotVisible: "(?s).*Perpanjang.*"
|
||||
- assertNotVisible: "(?s).*Pengingat.*"
|
||||
- assertNotVisible: "(?s).*Opening protocol.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-1-06-offline-no-tiles
|
||||
@@ -0,0 +1,74 @@
|
||||
# ts-mitra-2-01 — §2 Undangan: Curhat Baru tab empty state
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §2 + figma BestieInvites (v4.jsx)
|
||||
#
|
||||
# Walks: login → home → tap Chat tab in BestieTabBar → assert Undangan
|
||||
# screen renders, two tab labels visible, Curhat Baru is the default active
|
||||
# tab and shows the empty state copy.
|
||||
#
|
||||
# This test does NOT fire a customer blast — the goal is the empty-state
|
||||
# layout. ts-mitra-2-03 covers the populated case.
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000801"
|
||||
MITRA_DISPLAY_NAME: "Maestro Undangan Empty"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 10000
|
||||
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000801"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 15000
|
||||
|
||||
# Tap Chat tab in the bottom BestieTabBar. The label text is "Chat" — the
|
||||
# Home variant doesn't render that string elsewhere on screen so the regex
|
||||
# match is unique.
|
||||
- tapOn: "(?s).*Chat.*"
|
||||
|
||||
# Undangan screen renders with both tabs visible. Default active tab is
|
||||
# Curhat Baru → empty state copy is visible.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Undangan.*"
|
||||
timeout: 8000
|
||||
- assertVisible:
|
||||
text: "(?s).*Curhat Baru.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Perpanjang Curhat.*"
|
||||
- assertVisible:
|
||||
text: "(?s).*Belum ada undangan masuk.*"
|
||||
|
||||
- takeScreenshot: ts-mitra-2-01-curhat-baru-empty
|
||||
@@ -0,0 +1,69 @@
|
||||
# ts-mitra-2-02 — §2 Undangan: Perpanjang Curhat tab empty state
|
||||
# Spec ref: requirement/flow_mitra.mermaid.md §2 + figma BestieInvitesExtend (v5.jsx)
|
||||
#
|
||||
# Same as 2-01 but taps into the second tab (Perpanjang Curhat) and asserts
|
||||
# its dedicated empty-state copy. The Perpanjang tab today is a placeholder
|
||||
# until the backend exposes a queryable list of pending extension invitations
|
||||
# (see undangan_screen.dart::_PerpanjangTab TODO), so the empty state is the
|
||||
# only verifiable visual.
|
||||
appId: com.mybestie.mitra
|
||||
env:
|
||||
TEST_PHONE: "+628200000802"
|
||||
MITRA_DISPLAY_NAME: "Maestro Perpanjang Empty"
|
||||
BACKEND_INTERNAL_URL: http://localhost:3001
|
||||
---
|
||||
- runScript:
|
||||
file: ../scripts/seed_mitra.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
MITRA_DISPLAY_NAME: ${MITRA_DISPLAY_NAME}
|
||||
IS_ACTIVE: "true"
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- runScript:
|
||||
file: ../scripts/reset_phone.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Halo Mitra Bestie.*"
|
||||
timeout: 10000
|
||||
|
||||
- tapOn:
|
||||
point: "50%, 53%"
|
||||
- inputText: "8200000802"
|
||||
- tapOn: "(?s).*kirim kode.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*masukin 6 digit kode.*"
|
||||
timeout: 10000
|
||||
- runScript:
|
||||
file: ../scripts/peek_otp.js
|
||||
env:
|
||||
TEST_PHONE: ${TEST_PHONE}
|
||||
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL}
|
||||
- inputText: ${output.OTP}
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Kamu lagi (ONLINE|OFFLINE).*"
|
||||
timeout: 15000
|
||||
|
||||
# Navigate to Undangan via the Chat tab.
|
||||
- tapOn: "(?s).*Chat.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Undangan.*"
|
||||
timeout: 8000
|
||||
|
||||
# Switch to the Perpanjang Curhat tab.
|
||||
- tapOn: "(?s).*Perpanjang Curhat.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "(?s).*Belum ada permintaan perpanjangan.*"
|
||||
timeout: 5000
|
||||
|
||||
- takeScreenshot: ts-mitra-2-02-perpanjang-empty
|
||||