Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 36 additions & 31 deletions .maestro/flows/001_001_invite_users_using_qr_code_or_link_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ onFlowComplete:
file: "scripts/teardown.js"

---
# Invite new user:
# Invite Teacher as new user:
- runFlow: "subflows/school_admin_login_flow.yaml"
- assertVisible:
id: "app_title"
Expand Down Expand Up @@ -96,6 +96,10 @@ onFlowComplete:
- tapOn: "Password*"
- inputText: "test123"
- tapOn: "Sign-up"
- runFlow:
when:
visible: "Save password for Respect?"
file: "subflows/save_password_prompt_cancel.yaml"
- assertVisible:
id: "app_title"
text: "Apps"
Expand All @@ -118,7 +122,7 @@ onFlowComplete:
- copyTextFrom:
id: "invite_code"

# C) Student sign-up via invite code to the class
# Student sign-up via invite code to the class
- clearState: world.respect.app
- launchApp:
arguments:
Expand Down Expand Up @@ -173,23 +177,14 @@ onFlowComplete:
text: "Waiting for approval"
- assertVisible: "Please wait"

# H) Teacher approve student's request to join the class
# Teacher approve student's request to join the class
- clearState: world.respect.app
- launchApp:
arguments:
respect_directory: ${output.SCHOOL_URL}
- tapOn: "Get Started"
- runFlow:
file: "subflows/get_started_select_school_by_name.yaml"
file: "subflows/school_user_login_flow.yaml"
env:
SCHOOL_NAME: ${SCHOOL_NAME}
- tapOn:
id: "username"
- inputText: "teacherauser"
- tapOn:
id : "password"
- inputText: "test123"
- tapOn: "Login"
USER_NAME: "teacherauser"
USER_PASSWORD: "test123"
- assertVisible: "Apps"
- tapOn: "Classes"
- assertVisible:
Expand All @@ -199,15 +194,25 @@ onFlowComplete:
- assertVisible: "Pending requests.*"
- assertVisible: "Student User.*"
- tapOn: "Accept Invite"
- assertNotVisible: "Pending requests.*"

# E) Teacher share invite link for parent device based signup
- tapOn: "Add student"
# Teacher send family member invite to Parent User
- tapOn: "Student User"
- assertVisible:
id: "app_title"
text: "Student User"
- tapOn:
id: "floating_action_button"
- assertVisible:
id: "app_title"
text: "Edit person"
- tapOn: "Family member"
- tapOn: "Invite person"
- tapOn: "Invite via parents"
- assertVisible: "Student User / Family member"
- tapOn: "Approval required" # turn the switch off
- assertVisible: "Approval not required until:.*"
- copyTextFrom:
id: "invite_url"
id: "invite_url"

# Parent sign-up to the app using invite link
- runFlow:
Expand All @@ -220,10 +225,10 @@ onFlowComplete:
text: "Invitation"
- assertVisible: "Role"
- assertVisible: "Parent"
- assertVisible: "Child"
- assertVisible: "Student User"
- assertVisible: "School name"
- assertVisible: ${output.SCHOOL_NAME}
- assertVisible: "Class name"
- assertVisible: "New Class"
- assertVisible: "School server URL"
- assertVisible: ${output.SCHOOL_URL}
- tapOn: "Next"
Expand Down Expand Up @@ -254,13 +259,13 @@ onFlowComplete:
- tapOn: "Password*"
- inputText: "test123"
- tapOn: "Sign-up"
- tapOn: "Child's name*"
- inputText: "Student User2"
- tapOn: "Gender*"
- tapOn: "Female"
- tapOn: "Child's date of birth*"
- runScript:
file: "scripts/setDate.js"
- inputText: ${output.pastYearDateC}
- tapOn: "Done"
# 1/Feb/2026: MD: Maestro AGAIN does not actually deliver the tap. Thanks.
- runFlow:
when:
visible: "Save password for Respect?"
file: "subflows/save_password_prompt_cancel.yaml"
- assertVisible:
id: "app_title"
text: "Apps"
- tapOn: "People"
- tapOn: "Student User"
- assertVisible: "Parent User"
26 changes: 26 additions & 0 deletions .maestro/flows/subflows/school_user_login_flow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
appId: world.respect.app

---
- launchApp:
arguments:
respect_directory: ${output.SCHOOL_URL}

- tapOn: "Get Started"

- runFlow:
file: "get_started_select_school_by_name.yaml"
env:
SCHOOL_NAME: ${SCHOOL_NAME}

- tapOn:
id: "username"
- inputText: ${USER_NAME}
- tapOn:
id : "password"
- inputText: ${USER_PASSWORD}
- tapOn: "Login"
- runFlow:
when:
visible: "Save password for Respect?"
file: "save_password_prompt_cancel.yaml"

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import world.respect.app.components.uiTextStringResource
import world.respect.datalayer.school.model.ClassInvite
import world.respect.datalayer.school.model.NewUserInvite
import world.respect.shared.generated.resources.Res
import world.respect.shared.generated.resources.child
import world.respect.shared.generated.resources.class_name
import world.respect.shared.generated.resources.loading
import world.respect.shared.generated.resources.next
Expand Down Expand Up @@ -122,7 +123,16 @@ fun AcceptInviteScreen(
}

else -> {
//Do nothing else
RespectDetailField(
modifier = Modifier.defaultItemPadding(),
label = { Text(stringResource(Res.string.role)) },
value = { Text(stringResource(invite.roleLabel)) }
)
RespectDetailField(
modifier = Modifier.defaultItemPadding(),
label = { Text(stringResource(Res.string.child)) },
value = { Text(uiState.inviteInfo?.childName?:"") }
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
Expand Down Expand Up @@ -50,6 +51,7 @@ import world.respect.shared.generated.resources.Res
import world.respect.shared.generated.resources.approval_not_required_until
import world.respect.shared.generated.resources.approval_required
import world.respect.shared.generated.resources.copy_link
import world.respect.shared.generated.resources.family_member
import world.respect.shared.generated.resources.invite_code_label
import world.respect.shared.generated.resources.invite_students_directly
import world.respect.shared.generated.resources.invite_via_email
Expand Down Expand Up @@ -135,6 +137,15 @@ fun InvitePersonScreen(

uiState.inviteUrl?.also { link ->
val linkStr = link.toString()
uiState.childName?.let {
Text(
text = "$it / "+stringResource(Res.string.family_member),
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp),
)
}

Image(
painter = rememberQrCodePainter(linkStr),
contentDescription = stringResource(Res.string.qr_code),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import world.respect.datalayer.db.school.adapters.toModel
import world.respect.datalayer.exceptions.ForbiddenException
import world.respect.datalayer.school.InviteDataSource
import world.respect.datalayer.school.InviteDataSourceLocal
import world.respect.datalayer.school.PersonDataSourceLocal
import world.respect.datalayer.school.domain.CheckPersonPermissionUseCase
import world.respect.datalayer.school.model.ClassInvite
import world.respect.datalayer.school.model.EnrollmentRoleEnum
Expand Down Expand Up @@ -114,7 +115,23 @@ class InviteDataSourceDb(

else -> {
//Family member invite
val knownPersonUid = (inviteToStore as? FamilyMemberInvite)?.personUid ?: "0"
val knownPersonUid =
(inviteToStore as? FamilyMemberInvite)?.personUid ?: "0"
val authGuid = authenticatedUser.guid

if (authGuid != knownPersonUid) {
val hasWrite = checkPersonPermissionUseCase(
otherPersonUid = knownPersonUid,
otherPersonKnownRole = null,
permissionsRequiredByRole = CheckPersonPermissionUseCase.PermissionsRequiredByRole.WRITE_PERMISSIONS
)

if (!hasWrite) {
throw Exception(
"user does not have permission to create family invite for $knownPersonUid"
)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import world.respect.datalayer.school.model.Invite2
class RespectInviteInfo(
val classGuid: String?=null,
val className: String?=null,
val childName: String?=null,
val userInviteType: UserInviteType?=null,
val invite: Invite2? = null
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,15 @@ data class ClassInvite(
data class FamilyMemberInvite(
override val uid: String,
override val code: String,
override val approvalRequiredAfter: InstantAsISO8601,
override val approvalRequiredAfter: InstantAsISO8601= Instant.fromEpochMilliseconds(0),
override val lastModified: InstantAsISO8601 = Clock.System.now(),
override val stored: InstantAsISO8601 = Clock.System.now(),
override val status: StatusEnum = StatusEnum.ACTIVE,
val personUid: String,
): Invite2
): Invite2 {
companion object {
fun uidFor(personUid: String?): String = "$TYPE_FAMILY_MEMBER:$personUid"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}


Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@
<string name="system_administrator">System administrator</string>
<string name="site_administrator">Site administrator</string>
<string name="role">Role</string>
<string name="child">Child</string>

<string name="date">Date</string>
<string name="time">Time</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,11 @@ data class InvitePerson(
val inviteUid: String,
): InvitePersonOptions

@Serializable
@SerialName("family")
data class FamilyInviteOptions(
val personUid: String
) : InvitePersonOptions

@Transient
val invitePersonOptions: InvitePersonOptions = Json.decodeFromString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState
import kotlin.collections.first
import kotlin.getValue
import kotlin.time.Clock
import world.respect.datalayer.school.model.FamilyMemberInvite
import world.respect.datalayer.school.model.Invite2
import world.respect.datalayer.school.model.StatusEnum
import kotlin.time.Duration.Companion.minutes

data class PersonEditUiState(
val uid: String = "",
Expand Down Expand Up @@ -298,12 +302,12 @@ class PersonEditViewModel(
NavCommand.Navigate(
PersonList.create(
filterByRole = filterByRole,
personGuid = route.guid,
personGuid = FamilyMemberInvite.uidFor(route.guid),
resultDest = RouteResultDest(
resultPopUpTo = route,
resultKey = PERSON_SELECT_RESULT
),
hideInvite = true,
hideInvite = false,
)
)
)
Expand Down Expand Up @@ -392,6 +396,19 @@ class PersonEditViewModel(
familyMembersAdded + familyMembersRemoved + personToSave.copy(lastModified = modTime)
)

if (route.guid == null && personToSave.roles.any { it.roleEnum == PersonRoleEnum.STUDENT }) {
val invite = FamilyMemberInvite(
uid = FamilyMemberInvite.uidFor(personToSave.guid),
code = Invite2.newRandomCode(),
approvalRequiredAfter = modTime + Invite2.APPROVAL_NOT_REQUIRED_INTERVAL_MINS.minutes,
lastModified = modTime,
stored = modTime,
status = StatusEnum.ACTIVE,
personUid = personToSave.guid
)
schoolDataSource.inviteDataSource.store(listOf(invite))
}

if(
!navResultReturner.sendResultIfResultExpected(
route = route,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import world.respect.datalayer.DataLoadState
import world.respect.datalayer.DataLoadingState
import world.respect.datalayer.NoDataLoadedState
import world.respect.datalayer.SchoolDataSource
import world.respect.datalayer.db.school.ext.fullName
import world.respect.datalayer.ext.dataOrNull
import world.respect.datalayer.school.domain.GetWritableRolesListUseCase
import world.respect.datalayer.school.ext.copyInvite
Expand All @@ -30,6 +31,7 @@ import world.respect.datalayer.school.ext.newUserInviteUid
import world.respect.datalayer.school.model.ClassInvite
import world.respect.datalayer.school.model.ClassInviteModeEnum
import world.respect.datalayer.school.model.EnrollmentRoleEnum
import world.respect.datalayer.school.model.FamilyMemberInvite
import world.respect.datalayer.school.model.Invite2
import world.respect.datalayer.school.model.NewUserInvite
import world.respect.datalayer.school.model.PersonRoleEnum
Expand Down Expand Up @@ -62,6 +64,7 @@ data class InvitePersonUiState(
val selectedRole: PersonRoleEnum? = null,
val className: String? = null,
val schoolName: String? = null,
val childName: String? = null,
val roleOptions: List<PersonRoleEnum> = emptyList()
) {
val inviteCode: String?
Expand Down Expand Up @@ -132,6 +135,7 @@ class InvitePersonViewModel(
}

_inviteUid.value = (route.invitePersonOptions as? InvitePerson.ClassInviteOptions)?.inviteUid
?: (route.invitePersonOptions as? InvitePerson.FamilyInviteOptions)?.personUid
?: selectedRole.newUserInviteUid

_inviteUid.collectLatest { inviteUid ->
Expand All @@ -140,6 +144,17 @@ class InvitePersonViewModel(
uid = inviteUid,
loadParams = DataLoadParams()
).collectLatest { invite ->
(invite.dataOrNull() as? FamilyMemberInvite)?.let { familyInvite ->
val childPerson= schoolDataSource.personDataSource.findByGuid(
loadParams = DataLoadParams(),
guid = familyInvite.personUid
).dataOrNull()
_uiState.update { prev ->
prev.copy(
childName = childPerson?.fullName(),
)
}
}
_uiState.update { prev ->
prev.copy(
invite = invite,
Expand Down
Loading