feat(mobile): keep team, template, and report flows in sync with the server

Team, meeting, memo, template, command, and dictionary screens drifted from
the server contract, and report submission could hang instead of confirming
to the user. The screens now use the server responses directly.

The retired Expo shell is removed; the React Native app is the mobile client.
Gradle-generated vector-icon drawables are ignored rather than committed.
This commit is contained in:
Yun Chan 2026-09-16 23:24:55 +09:00
parent bb0e54dcee
commit 94d8bb8ebe
32 changed files with 310 additions and 2229 deletions

View file

@ -29,11 +29,13 @@ import {
acceptTeamInvite,
cancelTeamInvite,
createTeam,
createTeamActivity,
createTeamInvite,
extractInviteToken,
normalizeInviteEmail,
normalizeInviteToken,
normalizeInviteUrl,
normalizeTeamActivity,
normalizeTeamInvite,
normalizeTeamMember,
normalizeTeamName,
@ -293,6 +295,57 @@ describe('team mutation safety', () => {
})
})
describe('team activity feed contract', () => {
test('normalizes a valid activity and rejects malformed rows', () => {
const activity = normalizeTeamActivity({
id: INVITE_ID,
team_id: TEAM_ID,
actor_id: USER_ID,
kind: 'note',
body: 'hello',
created_at: '2026-01-01T00:00:00.000Z',
})
expect(activity).toEqual({
id: INVITE_ID,
teamId: TEAM_ID,
actorId: USER_ID,
kind: 'note',
body: 'hello',
createdAt: '2026-01-01T00:00:00.000Z',
})
expect(() => normalizeTeamActivity({ id: 'not-a-uuid', team_id: TEAM_ID, kind: 'note' }))
.toThrow(TeamServiceError)
})
test('posts a trimmed note through the RPC and rejects empty notes', async () => {
mockRpc.mockResolvedValue({
data: {
id: INVITE_ID,
team_id: TEAM_ID,
actor_id: USER_ID,
kind: 'note',
body: 'hello',
created_at: '2026-01-01T00:00:00.000Z',
},
error: null,
})
const created = await createTeamActivity(TEAM_ID, 'note', ' hello ')
expect(mockRpc).toHaveBeenCalledWith('create_team_activity', {
p_team_id: TEAM_ID,
p_kind: 'note',
p_body: 'hello',
p_metadata: {},
})
expect(created.body).toBe('hello')
mockRpc.mockClear()
await expect(createTeamActivity(TEAM_ID, 'note', ' ')).rejects.toMatchObject({
code: 'validation',
})
expect(mockRpc).not.toHaveBeenCalled()
})
})
describe('team realtime contract', () => {
test('filters every mutable team table by the selected team', () => {
mockRealtimeOn.mockClear()
@ -306,6 +359,7 @@ describe('team realtime contract', () => {
{ event: '*', schema: 'public', table: 'team_members', filter: `team_id=eq.${TEAM_ID}` },
{ event: '*', schema: 'public', table: 'team_invites', filter: `team_id=eq.${TEAM_ID}` },
{ event: '*', schema: 'public', table: 'meetings', filter: `team_id=eq.${TEAM_ID}` },
{ event: '*', schema: 'public', table: 'team_activities', filter: `team_id=eq.${TEAM_ID}` },
])
})
})