Mitra Profil: WA/TG contacts + Keluar-only (no Hapus Akun)

Replaces the placeholder "Hubungi Koordinator" row with two real
contacts pulled from backend config (support_handles_json), and drops
the "Hapus Akun" CTA. Mirrors the figma BestieProfile design but uses
the same WA/TG channel as the customer Tanya Admin sheet — business
decided the same ops team triages both audiences.

Backend:
- Promote support-handles route from /api/client to /api/shared
  (renamed file + export). Both apps now consume the same endpoint;
  hitting /api/client/* from mitra would violate the per-app
  convention in mitra_app/CLAUDE.md.
- client_app provider updated to /api/shared/support-handles.

Mitra app:
- New support_handles_provider mirroring the client_app one. Adds a
  `displayHandle` getter that strips the URL scheme for the subtitle
  ("https://wa.me/X" → "wa.me/X", "https://t.me/Y" → "t.me/Y") so the
  row looks like the figma without exposing raw URLs.
- Profil screen now lists: Chat WhatsApp Kami, Chat Telegram Kami,
  Syarat & Ketentuan, Kebijakan Privasi. Danger zone simplified to
  Keluar only — mitras request account deletion through the same
  WA/TG channels (no separate self-service path).
- url_launcher added as a runtime dep, launches deeplinks in
  externalApplication mode with graceful snackbar fallback when
  parsing or launching fails.

Updates [[feedback-mitra-internal-audience]] — pre-login rule still
holds (no admin CTAs on S3a/S3b/AccountInactive), but the post-login
Profil tab now does surface WA/TG. Overrides decided 2026-05-21.

Verified on emulator-5556: Profil tab renders both rows with handles
from `wa.me/6285173310010` + `t.me/halobestie`, Keluar present, no
Hapus Akun button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 16:32:21 +08:00
parent e4bffe1a71
commit 10699d1ad1
7 changed files with 174 additions and 99 deletions

View File

@@ -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)

View File

@@ -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 })