generated from ttipo/project-base
feat(flutter): 앱 링크 — 기기 카메라로 찍은 QR 로 바로 받기·참여·연동
QR 을 앱 스캐너가 아니라 기기 카메라로 찍어도 앱이 열려 이어지게 한다. - rup://app/<경로> 링크 (Android intent-filter · iOS URL scheme) — Android 는 호스트를 버리고 경로만 넘기므로 경로로 구분한다 - 명함 → 저장 확인 화면, 다이어리 초대 → 참여 확인, 일정 초대 → 연동 확인 (끝나면 그 기능의 탭으로) - QR 에 담는 주소를 새 형식으로 바꾸고, 예전 형식(rup://pet-card/…)도 계속 읽는다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:tma_app/core/config/app_links.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary_invitation.dart';
|
||||
import 'package:tma_app/features/pet_card/data/received_card_repository.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/card_style.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/card_view_mode.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/received_card.dart';
|
||||
import 'package:tma_app/features/pet_card/presentation/pages/receive_card_page.dart';
|
||||
import 'package:tma_app/features/schedule/data/calendar_sharing_repository.dart';
|
||||
import 'package:tma_app/features/schedule/domain/calendar_sharing.dart';
|
||||
import 'package:tma_app/features/schedule/presentation/pages/share_calendar_page.dart';
|
||||
import 'package:tma_app/features/shell/presentation/pages/main_shell.dart';
|
||||
|
||||
import 'support/app_harness.dart';
|
||||
|
||||
/// 명함 미리보기만 하는 저장소
|
||||
class _CardRepo implements ReceivedCardRepository {
|
||||
final previewed = <String>[];
|
||||
|
||||
@override
|
||||
bool get available => true;
|
||||
|
||||
@override
|
||||
Future<List<ReceivedCard>> fetch() async => const [];
|
||||
|
||||
@override
|
||||
Future<SharedCard> preview(String code) async {
|
||||
previewed.add(code);
|
||||
return SharedCard(
|
||||
code: code,
|
||||
mode: CardViewMode.simple,
|
||||
senderNickname: '초코아빠',
|
||||
senderPhotoPath: null,
|
||||
pet: samplePet(id: 'shared', name: '초코'),
|
||||
style: CardStyle.initial(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ReceivedCard> receive(String code, {String? note}) => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {}
|
||||
}
|
||||
|
||||
/// 함께 관리 — 연동 전, 받은 코드는 콩이아빠의 것
|
||||
class _SharingRepo implements CalendarSharingRepository {
|
||||
bool linked = false;
|
||||
final joined = <String>[];
|
||||
|
||||
CalendarSharing get _sharing => CalendarSharing(
|
||||
inviteCode: 'RUP-MINE00',
|
||||
linked: linked,
|
||||
members: [
|
||||
const CalendarPerson(id: 'me', isMe: true),
|
||||
if (linked) const CalendarPerson(id: 'dad', nickname: '콩이아빠'),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
bool get available => true;
|
||||
|
||||
@override
|
||||
Future<CalendarSharing?> fetch({bool quiet = false}) async {
|
||||
return _sharing;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CalendarInvitation> previewInvitation(String code) async => CalendarInvitation(
|
||||
code: code,
|
||||
ownerNickname: '콩이아빠',
|
||||
memberCount: 1,
|
||||
maxMembers: 4,
|
||||
isMine: false,
|
||||
isMember: false,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<CalendarSharing> join(String code) async {
|
||||
joined.add(code);
|
||||
linked = true;
|
||||
return _sharing;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> leave() async => linked = false;
|
||||
}
|
||||
|
||||
Future<GoRouter> _launch(WidgetTester tester, {ReceivedCardRepository? cards, CalendarSharingRepository? sharing}) async {
|
||||
await launchSignedIn(
|
||||
tester,
|
||||
extra: {'pets.v1': jsonEncode([samplePet().toJson()])},
|
||||
overrides: [
|
||||
if (cards != null) receivedCardRepositoryProvider.overrideWithValue(cards),
|
||||
if (sharing != null) calendarSharingRepositoryProvider.overrideWithValue(sharing),
|
||||
],
|
||||
);
|
||||
return GoRouter.of(tester.element(find.byType(MainShell)));
|
||||
}
|
||||
|
||||
/// 기기 카메라로 찍은 링크가 들어온 것처럼 — 앱에는 링크의 경로만 들어온다
|
||||
Future<void> _openLink(WidgetTester tester, GoRouter router, String link) async {
|
||||
router.go(Uri.parse(link).path);
|
||||
await settle(tester);
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('앱 링크 주소', () {
|
||||
test('QR 에 담는 주소는 rup://app/ 아래 경로다', () {
|
||||
expect(petCardDeepLink('0428-117'), 'rup://app/pet-card/0428-117');
|
||||
expect(diaryInviteLink('RUP-8F3K2Q'), 'rup://app/diary-invite/RUP-8F3K2Q');
|
||||
expect(calendarInviteLink('RUP-8K2D7Q'), 'rup://app/calendar-invite/RUP-8K2D7Q');
|
||||
expect(Uri.parse(petCardDeepLink('0428-117')).path, '/pet-card/0428-117');
|
||||
});
|
||||
|
||||
test('새 주소와 예전 주소를 모두 코드로 읽는다', () {
|
||||
expect(parseCardCode('rup://app/pet-card/0428-117'), '0428-117');
|
||||
expect(parseCardCode('rup://pet-card/0428-117'), '0428-117');
|
||||
expect(parseInviteCode('rup://app/diary-invite/RUP-8F3K2Q'), 'RUP-8F3K2Q');
|
||||
expect(parseCalendarCode('rup://app/calendar-invite/rup-8k2d7q'), 'RUP-8K2D7Q');
|
||||
expect(stripAppLink('0428-117', kCardLinkSegment), '0428-117');
|
||||
});
|
||||
|
||||
test('다른 기능의 링크는 코드로 읽지 않는다', () {
|
||||
expect(parseCalendarCode('rup://app/diary-invite/RUP-8F3K2Q'), isNull);
|
||||
expect(parseInviteCode('rup://app/calendar-invite/RUP-8K2D7Q'), isNull);
|
||||
expect(parseCardCode('rup://app/diary-invite/RUP-8F3K2Q'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('명함 링크로 열면 명함을 불러와 저장 확인 화면을 연다', (tester) async {
|
||||
final cards = _CardRepo();
|
||||
final router = await _launch(tester, cards: cards);
|
||||
|
||||
await _openLink(tester, router, petCardDeepLink('0428-117'));
|
||||
|
||||
expect(cards.previewed, ['0428-117']);
|
||||
expect(find.byType(ReceiveCardPage), findsOneWidget);
|
||||
expect(find.text('초코의 명함'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('명함 코드가 아닌 링크는 서버에 묻지 않고 펫 명함 탭으로 보낸다', (tester) async {
|
||||
final cards = _CardRepo();
|
||||
final router = await _launch(tester, cards: cards);
|
||||
|
||||
await _openLink(tester, router, 'rup://app/pet-card/hello');
|
||||
|
||||
expect(cards.previewed, isEmpty);
|
||||
expect(find.text('명함을 찾을 수 없어요. 명함 코드를 다시 확인해 주세요.'), findsOneWidget);
|
||||
expect(find.text('우리 아이 명함'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('일정 연동 링크로 열면 확인한 뒤 연동하고 함께 관리 화면을 연다', (tester) async {
|
||||
final sharing = _SharingRepo();
|
||||
final router = await _launch(tester, sharing: sharing);
|
||||
|
||||
await _openLink(tester, router, calendarInviteLink('RUP-DAD123'));
|
||||
expect(find.text('일정을 공유할까요?'), findsOneWidget);
|
||||
await tester.tap(find.text('공유하기'));
|
||||
await settle(tester);
|
||||
await settle(tester);
|
||||
|
||||
expect(sharing.joined, ['RUP-DAD123']);
|
||||
expect(find.byType(ShareCalendarPage), findsOneWidget);
|
||||
expect(find.text('2/4'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user