Client Profile: save-phone banner for anonymous users
Anonymous customers now see a brand-gradient "Simpan Nomor HP" panel above the user card on the kamu tab, ported from the Figma SProfile save-phone banner. Tapping it pushes /auth/register?from=profile, which hides the "lanjut tanpa verifikasi (harga normal)" link — a user who re-entered the verif funnel from Profile shouldn't be re-offered the anon exit. Spec §1.3 added documenting the ?from= entry-point convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,12 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
final name = _greetingName(authState.valueOrNull);
|
||||
final shownName = name.isEmpty ? 'kamu' : name;
|
||||
|
||||
// When the user arrives via the Profile "Simpan Nomor HP" CTA they've
|
||||
// already committed to identifying — drop the anonymous escape hatch so
|
||||
// there's only one way forward.
|
||||
final fromProfile =
|
||||
GoRouterState.of(context).uri.queryParameters['from'] == 'profile';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: HaloTokens.bg,
|
||||
body: SafeArea(
|
||||
@@ -207,6 +213,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
ref.read(authProvider.notifier).requestOtp(_e164Phone())
|
||||
: null,
|
||||
),
|
||||
if (!fromProfile) ...[
|
||||
const SizedBox(height: 4),
|
||||
TextButton(
|
||||
onPressed: isLoading
|
||||
@@ -229,6 +236,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/auth/auth_notifier.dart';
|
||||
import '../../core/theme/halo_tokens.dart';
|
||||
import '../home/widgets/halo_tab_bar.dart';
|
||||
@@ -19,6 +20,7 @@ class ProfileScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final authData = ref.watch(authProvider).valueOrNull;
|
||||
final isAnonymous = authData is AuthAnonymousData;
|
||||
|
||||
final (name, phone) = switch (authData) {
|
||||
AuthAuthenticatedData d => (
|
||||
@@ -53,6 +55,12 @@ class ProfileScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
if (isAnonymous) ...[
|
||||
_SavePhoneBanner(
|
||||
onTap: () => context.push('/auth/register?from=profile'),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
_UserCard(name: name, phone: phone),
|
||||
const SizedBox(height: 20),
|
||||
_MenuCard(items: [
|
||||
@@ -154,6 +162,94 @@ class ProfileScreen extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SavePhoneBanner extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const _SavePhoneBanner({required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [HaloTokens.brandSofter, HaloTokens.brandSoft],
|
||||
),
|
||||
borderRadius: HaloRadius.lg,
|
||||
border: Border.all(color: HaloTokens.brand, width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: HaloTokens.brand.withValues(alpha: 0.10),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
color: HaloTokens.brand,
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.smartphone,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Biar riwayat curhat kamu tersimpan, yuk simpan Nomor Handphone kamu…',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: HaloTokens.brandDark,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Material(
|
||||
color: HaloTokens.brand,
|
||||
borderRadius: HaloRadius.md,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: HaloRadius.md,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'Simpan Nomor HP',
|
||||
style: TextStyle(
|
||||
fontFamily: HaloTokens.fontBody,
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserCard extends StatelessWidget {
|
||||
final String name;
|
||||
final String? phone;
|
||||
|
||||
@@ -85,6 +85,17 @@ this doc.
|
||||
### 1.2 Backend
|
||||
- New: `GET /api/client/onboarding-state` → returns `{ has_paid_first_session: bool }`. Used by client_app to pick whether to show this sheet.
|
||||
|
||||
### 1.3 Profile re-prompt for anon users (shipped 2026-05-22)
|
||||
|
||||
> Figma: `screens/extras.jsx::SProfile` save-phone banner (lines 170-204).
|
||||
> Code: `client_app/lib/features/profile/profile_screen.dart::_SavePhoneBanner`.
|
||||
|
||||
For users who chose the anon path in §1 and later open the **kamu** (Profile) tab, a brand-gradient banner appears between the page title and the user card. Body copy: *"Biar riwayat curhat kamu tersimpan, yuk simpan Nomor Handphone kamu…"*. CTA `Simpan Nomor HP` pushes `/auth/register?from=profile`.
|
||||
|
||||
- Banner gates on `authData is AuthAnonymousData` — disappears the moment the auth state flips to `AuthAuthenticatedData` after a successful OTP verify.
|
||||
- The `?from=profile` query param causes `RegisterScreen` to **omit** the *"lanjut tanpa verifikasi (harga normal)"* escape-hatch link. A user who tapped the banner deliberately re-entered the verif funnel, so we don't re-offer the anon exit.
|
||||
- Convention for future entry points: when pushing into `/auth/register` from somewhere that should branch the screen (copy, escape hatch, post-OTP destination), pass `?from=<callsite-slug>` and read `GoRouterState.of(context).uri.queryParameters['from']` in `RegisterScreen.build()`. The router's redirect preserves query params for anonymous users on `/auth/*` routes.
|
||||
|
||||
## 2. ESP screening + USP screen
|
||||
|
||||
> Figma: `screens/onboarding.jsx::S5ESP` and `S5USP`. Existing
|
||||
|
||||
Reference in New Issue
Block a user