@ossy/users 3.11.1 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/users",
3
3
  "description": "User domain - aggregate, events, and validators for the Ossy user model",
4
- "version": "3.11.1",
4
+ "version": "3.12.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
@@ -11,7 +11,8 @@
11
11
  "./server": "./src/server.js",
12
12
  "./update-email.flow.js": "./src/update-email.flow.js",
13
13
  "./update-profile-name.flow.js": "./src/update-profile-name.flow.js",
14
- "./update-profile-details.schema.js": "./src/update-profile-details.schema.js"
14
+ "./update-profile-details.schema.js": "./src/update-profile-details.schema.js",
15
+ "./complete-user-name.schema.js": "./src/complete-user-name.schema.js"
15
16
  },
16
17
  "scripts": {
17
18
  "test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
@@ -49,5 +50,5 @@
49
50
  "/src",
50
51
  "README.md"
51
52
  ],
52
- "gitHead": "f279de3535b6bc76631cbc6b943dae8f92a1565f"
53
+ "gitHead": "c2650803219ad1831559b512848358d9550ffad2"
53
54
  }
@@ -0,0 +1,99 @@
1
+ import React, { useMemo, useState } from 'react'
2
+ import { View, Text, Button, Form, FieldFactory, FormStatus, useLocale } from '@ossy/design-system'
3
+ import { useRouter } from '@ossy/router-react'
4
+ import { Schema, contentValidator } from '@ossy/schema'
5
+ import { useSdk } from '@ossy/sdk-react'
6
+ import { SignOut } from '@ossy/authentication'
7
+ import { metadata as UpdateUserDetails } from './update-details.action.js'
8
+ import { metadata as CompleteUserNameAction } from './complete-user-name.action.js'
9
+ import { metadata as CompleteUserNameForm } from './complete-user-name.form.js'
10
+ import completeUserNameTemplate from './complete-user-name.schema.js'
11
+
12
+ /**
13
+ * Blocks product UI until a signed-in user has a first and last name.
14
+ */
15
+ export function CompleteUserName ({ onCompleted }) {
16
+ const { t } = useLocale()
17
+ const router = useRouter()
18
+ const sdk = useSdk()
19
+ const [saving, setSaving] = useState(false)
20
+ const [saveError, setSaveError] = useState(false)
21
+ const validate = useMemo(
22
+ () => contentValidator(
23
+ Schema.of({ schemas: [completeUserNameTemplate] }),
24
+ completeUserNameTemplate,
25
+ ),
26
+ [],
27
+ )
28
+
29
+ const signOff = () => {
30
+ const home = router.getHref('@home') || '/'
31
+ const redirect = encodeURIComponent(home)
32
+ window.location.href = `/api/v0/users/sign-off?redirect=${redirect}`
33
+ }
34
+
35
+ const saveName = (data) => {
36
+ setSaving(true)
37
+ setSaveError(false)
38
+ sdk.invoke(UpdateUserDetails, {
39
+ firstName: data.firstName,
40
+ lastName: data.lastName,
41
+ })
42
+ .then(() => onCompleted?.())
43
+ .catch(() => setSaveError(true))
44
+ .finally(() => setSaving(false))
45
+ }
46
+
47
+ return (
48
+ <View
49
+ layout="off-center-s"
50
+ inset="s"
51
+ surface="base"
52
+ style={{ height: '100%', minHeight: '100%' }}
53
+ data-flow-stage="complete-user-name"
54
+ >
55
+ <View inset="s" roundness="l" surface="primary" data-region="content" gap="l">
56
+ <Form
57
+ id={CompleteUserNameForm.id}
58
+ schemaId={CompleteUserNameForm.schemaId}
59
+ fields={completeUserNameTemplate.fields}
60
+ defaultData={{}}
61
+ onSubmit={saveName}
62
+ validate={validate}
63
+ status={saving ? [FormStatus.Submitting] : []}
64
+ gap="l"
65
+ >
66
+ <View gap="s">
67
+ <Text as="h1" variant="heading-tertiary" text="users.completeName.title" />
68
+ <Text text="users.completeName.description" />
69
+ </View>
70
+ {saveError && (
71
+ <View gap="xs" data-flow-stage="complete-user-name-error">
72
+ <Text as="h2" variant="heading-quaternary">
73
+ {t('users.completeName.errorTitle')}
74
+ </Text>
75
+ <Text>
76
+ {t('users.completeName.errorBody')}
77
+ </Text>
78
+ </View>
79
+ )}
80
+ <FieldFactory />
81
+ <View layout="row" gap="s" justifyContent="flex-end">
82
+ <Button
83
+ {...SignOut}
84
+ variant="link"
85
+ disabled={saving}
86
+ onClick={signOff}
87
+ />
88
+ <Button
89
+ {...CompleteUserNameAction}
90
+ type="submit"
91
+ variant="cta"
92
+ disabled={saving}
93
+ />
94
+ </View>
95
+ </Form>
96
+ </View>
97
+ </View>
98
+ )
99
+ }
@@ -0,0 +1,7 @@
1
+ /** Client-only — submits the signed-in display-name completion form. */
2
+ export const metadata = {
3
+ id: '@ossy/users/actions/complete-user-name',
4
+ access: 'authenticated',
5
+ icon: 'check',
6
+ label: '@ossy/users/actions/complete-user-name.label',
7
+ }
@@ -0,0 +1,5 @@
1
+ export const metadata = {
2
+ id: '@ossy/users/form/complete-user-name',
3
+ schemaId: '@ossy/users/schema/complete-user-name',
4
+ label: '@ossy/users/form/complete-user-name.label',
5
+ }
@@ -0,0 +1,10 @@
1
+ /** Required first/last name for signed-in users who were created email-only. */
2
+ export default {
3
+ id: '@ossy/users/schema/complete-user-name',
4
+ name: 'Complete user name',
5
+ icon: 'user',
6
+ fields: [
7
+ { name: 'firstName', type: 'text', required: true },
8
+ { name: 'lastName', type: 'text', required: true },
9
+ ],
10
+ }
@@ -37,5 +37,14 @@
37
37
  "users.verifyEmailChange.profileButton": "Back to profile",
38
38
  "users.emailChanged.subject": "Your Ossy email was changed",
39
39
  "users.emailChanged.title": "Email address updated",
40
- "users.emailChanged.body": "The email address on your Ossy account was changed. If you did not make this change, contact support."
40
+ "users.emailChanged.body": "The email address on your Ossy account was changed. If you did not make this change, contact support.",
41
+ "@ossy/users/actions/complete-user-name.label": "Continue",
42
+ "@ossy/users/actions/complete-user-name.description": "Save first and last name for the signed-in user",
43
+ "@ossy/users/form/complete-user-name.label": "Your name",
44
+ "@ossy/users/schema/complete-user-name.firstName.label": "First name",
45
+ "@ossy/users/schema/complete-user-name.lastName.label": "Last name",
46
+ "users.completeName.title": "What's your name?",
47
+ "users.completeName.description": "We use this on your profile and when you work with others.",
48
+ "users.completeName.errorTitle": "Could not save name",
49
+ "users.completeName.errorBody": "Something went wrong. Try again in a moment."
41
50
  }
package/src/index.js CHANGED
@@ -7,7 +7,11 @@ export { metadata as ConfirmEmailChange } from './confirm-email-change.action.js
7
7
  export { metadata as EditProfileDetails } from './edit-profile-details.action.js'
8
8
  export { metadata as SaveProfileDetails } from './save-profile-details.action.js'
9
9
  export { metadata as UpdateProfileDetailsForm } from './update-profile-details.form.js'
10
+ export { metadata as CompleteUserName } from './complete-user-name.action.js'
11
+ export { metadata as CompleteUserNameForm } from './complete-user-name.form.js'
10
12
  export { metadata as GetCurrentUserHistory } from './get-current-user-history.action.js'
11
13
  export { metadata as JoinWorkspace } from './join-workspace.action.js'
12
14
  export { metadata as LeaveWorkspace } from './leave-workspace.action.js'
13
15
  export { VerifyEmailChange } from './VerifyEmailChange.jsx'
16
+ export { CompleteUserName as CompleteUserNamePrompt } from './CompleteUserName.jsx'
17
+ export { outstandingUserRequirements, userNeedsDisplayName } from './user-requirements.js'
@@ -37,5 +37,14 @@
37
37
  "users.verifyEmailChange.profileButton": "Tillbaka till profil",
38
38
  "users.emailChanged.subject": "Din Ossy-e-post har ändrats",
39
39
  "users.emailChanged.title": "E-postadress uppdaterad",
40
- "users.emailChanged.body": "E-postadressen på ditt Ossy-konto har ändrats. Om du inte gjorde ändringen, kontakta support."
40
+ "users.emailChanged.body": "E-postadressen på ditt Ossy-konto har ändrats. Om du inte gjorde ändringen, kontakta support.",
41
+ "@ossy/users/actions/complete-user-name.label": "Fortsätt",
42
+ "@ossy/users/actions/complete-user-name.description": "Spara för- och efternamn för den inloggade användaren",
43
+ "@ossy/users/form/complete-user-name.label": "Ditt namn",
44
+ "@ossy/users/schema/complete-user-name.firstName.label": "Förnamn",
45
+ "@ossy/users/schema/complete-user-name.lastName.label": "Efternamn",
46
+ "users.completeName.title": "Vad heter du?",
47
+ "users.completeName.description": "Vi använder namnet på din profil och när du arbetar med andra.",
48
+ "users.completeName.errorTitle": "Kunde inte spara namnet",
49
+ "users.completeName.errorBody": "Något gick fel. Försök igen om en stund."
41
50
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Ordered product gates for a signed-in user. Name first; later entries can
3
+ * include a required user-agreement version without changing the shell.
4
+ *
5
+ * @param {{ firstName?: string, lastName?: string } | null | undefined} user
6
+ * @returns {string[]}
7
+ */
8
+ export function outstandingUserRequirements (user) {
9
+ if (!user) return []
10
+ const requirements = []
11
+ if (userNeedsDisplayName(user)) requirements.push('name')
12
+ return requirements
13
+ }
14
+
15
+ /**
16
+ * True when a signed-in user is missing a trimmed first or last name.
17
+ *
18
+ * @param {{ firstName?: string, lastName?: string } | null | undefined} user
19
+ * @returns {boolean}
20
+ */
21
+ export function userNeedsDisplayName (user) {
22
+ if (!user) return false
23
+ const firstName = typeof user.firstName === 'string' ? user.firstName.trim() : ''
24
+ const lastName = typeof user.lastName === 'string' ? user.lastName.trim() : ''
25
+ return !firstName || !lastName
26
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+ import { outstandingUserRequirements, userNeedsDisplayName } from './user-requirements.js'
3
+
4
+ describe('userNeedsDisplayName', () => {
5
+ it('is false for anonymous or missing user', () => {
6
+ expect(userNeedsDisplayName(undefined)).toBe(false)
7
+ expect(userNeedsDisplayName(null)).toBe(false)
8
+ })
9
+
10
+ it('requires both trimmed first and last name', () => {
11
+ expect(userNeedsDisplayName({})).toBe(true)
12
+ expect(userNeedsDisplayName({ firstName: 'Ada' })).toBe(true)
13
+ expect(userNeedsDisplayName({ lastName: 'Lovelace' })).toBe(true)
14
+ expect(userNeedsDisplayName({ firstName: ' ', lastName: 'Lovelace' })).toBe(true)
15
+ expect(userNeedsDisplayName({ firstName: 'Ada', lastName: ' ' })).toBe(true)
16
+ expect(userNeedsDisplayName({ firstName: 'Ada', lastName: 'Lovelace' })).toBe(false)
17
+ })
18
+ })
19
+
20
+ describe('outstandingUserRequirements', () => {
21
+ it('returns no gates for anonymous users', () => {
22
+ expect(outstandingUserRequirements(undefined)).toEqual([])
23
+ })
24
+
25
+ it('lists name first when the display name is missing', () => {
26
+ expect(outstandingUserRequirements({ firstName: '', lastName: '' })).toEqual(['name'])
27
+ expect(outstandingUserRequirements({ firstName: 'Ada', lastName: 'Lovelace' })).toEqual([])
28
+ })
29
+ })