Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for Client Side Token Generation #52

Merged
merged 1 commit into from
May 2, 2024
Merged
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
3 changes: 2 additions & 1 deletion dev-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
android:supportsRtl="true"
tools:targetApi="31">

<!-- The metadata consumed by the Dev App + SDK -->
<!-- Metadata Required for Server-Side Integration -->
<!-- This information is only consumed by the DevApp (not the SDK) to simulate a server side integration. -->
<meta-data android:name="uid2_api_key" android:value=""/>
<meta-data android:name="uid2_api_secret" android:value=""/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class AppUID2Client(
}

private fun decryptResponse(key: String, data: String) =
DataEnvelope.decrypt(key, data, false)?.toString(Charsets.UTF_8)
DataEnvelope.decrypt(key, data, true)?.toString(Charsets.UTF_8)

private fun Long.toByteArray() = ByteBuffer.allocate(Long.SIZE_BYTES).apply {
order(ByteOrder.BIG_ENDIAN)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.uid2.dev.network

import com.uid2.UID2Exception

/**
* The exception thrown when an error occurred in the Development Application's UID2 Client.
*/
class AppUID2ClientException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)
class AppUID2ClientException(message: String? = null, cause: Throwable? = null) : UID2Exception(message, cause)
63 changes: 61 additions & 2 deletions dev-app/src/main/java/com/uid2/dev/ui/MainScreen.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
package com.uid2.dev.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Checkbox
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Phone
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.uid2.dev.ui.MainScreenAction.EmailChanged
import com.uid2.dev.ui.MainScreenAction.PhoneChanged
import com.uid2.dev.ui.MainScreenAction.RefreshButtonPressed
import com.uid2.dev.ui.MainScreenAction.ResetButtonPressed
import com.uid2.dev.ui.MainScreenState.ErrorState
import com.uid2.dev.ui.MainScreenState.LoadingState
import com.uid2.dev.ui.MainScreenState.UserUpdatedState
import com.uid2.dev.ui.views.ActionButtonView
import com.uid2.dev.ui.views.EmailInputView
import com.uid2.dev.ui.views.ErrorView
import com.uid2.dev.ui.views.IdentityInputView
import com.uid2.dev.ui.views.LoadingView
import com.uid2.dev.ui.views.UserIdentityView
import com.uid2.devapp.R

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MainScreen(viewModel: MainScreenViewModel) {
val viewState by viewModel.viewState.collectAsState()
Expand All @@ -33,9 +52,49 @@ fun MainScreen(viewModel: MainScreenViewModel) {
)
},
) { padding ->
val checkedState = remember { mutableStateOf(true) }

Column(modifier = Modifier.padding(10.dp, 10.dp, 10.dp, 10.dp + padding.calculateBottomPadding())) {
// The top of the View provides a way for the Email Address to be entered.
EmailInputView(Modifier, onEmailEntered = { viewModel.processAction(EmailChanged(it)) })
IdentityInputView(
modifier = Modifier.padding(bottom = 6.dp),
label = stringResource(R.string.email),
icon = Icons.Default.Email,
onEntered = { viewModel.processAction(EmailChanged(it, checkedState.value)) },
)

IdentityInputView(
label = stringResource(R.string.phone),
icon = Icons.Default.Phone,
onEntered = { viewModel.processAction(PhoneChanged(it, checkedState.value)) },
)

Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
Checkbox(
modifier = Modifier
.padding(vertical = 4.dp)
.padding(end = 4.dp),
checked = checkedState.value,
onCheckedChange = { checkedState.value = it },
)
}

Text(text = stringResource(id = R.string.generate_client_side))
}

Divider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
thickness = 2.dp,
color = Color.Black,
)

// Depending on the state of the View Model, we will switch in different content view.
when (val state = viewState) {
Expand Down
64 changes: 56 additions & 8 deletions dev-app/src/main/java/com/uid2/dev/ui/MainScreenViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.uid2.UID2Exception
import com.uid2.UID2Manager
import com.uid2.UID2Manager.GenerateIdentityResult
import com.uid2.UID2ManagerState.Established
import com.uid2.UID2ManagerState.Expired
import com.uid2.UID2ManagerState.Loading
import com.uid2.UID2ManagerState.NoIdentity
import com.uid2.UID2ManagerState.OptOut
import com.uid2.UID2ManagerState.RefreshExpired
import com.uid2.UID2ManagerState.Refreshed
import com.uid2.data.IdentityRequest
import com.uid2.data.IdentityStatus
import com.uid2.data.IdentityStatus.ESTABLISHED
import com.uid2.data.IdentityStatus.EXPIRED
Expand All @@ -22,8 +25,8 @@ import com.uid2.data.IdentityStatus.REFRESHED
import com.uid2.data.IdentityStatus.REFRESH_EXPIRED
import com.uid2.data.UID2Identity
import com.uid2.dev.network.AppUID2Client
import com.uid2.dev.network.AppUID2ClientException
import com.uid2.dev.network.RequestType.EMAIL
import com.uid2.dev.network.RequestType.PHONE
import com.uid2.dev.ui.MainScreenState.ErrorState
import com.uid2.dev.ui.MainScreenState.LoadingState
import com.uid2.dev.ui.MainScreenState.UserUpdatedState
Expand All @@ -33,7 +36,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

sealed interface MainScreenAction : ViewModelAction {
data class EmailChanged(val address: String) : MainScreenAction
data class EmailChanged(val address: String, val clientSide: Boolean) : MainScreenAction
data class PhoneChanged(val number: String, val clientSide: Boolean) : MainScreenAction
data object ResetButtonPressed : MainScreenAction
data object RefreshButtonPressed : MainScreenAction
}
Expand Down Expand Up @@ -76,21 +80,60 @@ class MainScreenViewModel(
override fun processAction(action: MainScreenAction) {
Log.d(TAG, "Action: $action")

// If we are reported an error from generateIdentity's onResult callback, we will update our state to reflect it
val onGenerateResult: (GenerateIdentityResult) -> Unit = { result ->
when (result) {
is GenerateIdentityResult.Error -> viewModelScope.launch { _viewState.emit(ErrorState(result.ex)) }
else -> Unit
}
}

viewModelScope.launch {
when (action) {
is MainScreenAction.EmailChanged -> {
_viewState.emit(LoadingState)

try {
// For Development purposes, we are required to generate the initial Identity before then
// passing it onto the SDK to be managed.
_viewState.emit(LoadingState)
api.generateIdentity(action.address, EMAIL)?.let {
manager.setIdentity(it)
if (action.clientSide) {
// Generate the identity via Client Side Integration (client side token generation).
manager.generateIdentity(
IdentityRequest.Email(action.address),
SUBSCRIPTION_ID,
PUBLIC_KEY,
onGenerateResult,
)
} else {
// We're going to generate the identity as if we've obtained it via a backend service.
api.generateIdentity(action.address, EMAIL)?.let {
manager.setIdentity(it)
}
}
} catch (ex: AppUID2ClientException) {
} catch (ex: UID2Exception) {
_viewState.emit(ErrorState(ex))
}
}
is MainScreenAction.PhoneChanged -> {
_viewState.emit(LoadingState)

try {
if (action.clientSide) {
// Generate the identity via Client Side Integration (client side token generation).
manager.generateIdentity(
IdentityRequest.Phone(action.number),
SUBSCRIPTION_ID,
PUBLIC_KEY,
onGenerateResult,
)
} else {
// We're going to generate the identity as if we've obtained it via a backend service.
api.generateIdentity(action.number, PHONE)?.let {
manager.setIdentity(it)
}
}
} catch (ex: UID2Exception) {
_viewState.emit(ErrorState(ex))
}
}
MainScreenAction.RefreshButtonPressed -> {
manager.currentIdentity?.let { _viewState.emit(LoadingState) }
manager.refreshIdentity()
Expand All @@ -105,6 +148,11 @@ class MainScreenViewModel(

private companion object {
const val TAG = "MainScreenViewModel"

const val SUBSCRIPTION_ID = "toPh8vgJgt"

@Suppress("ktlint:standard:max-line-length")
const val PUBLIC_KEY = "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKAbPfOz7u25g1fL6riU7p2eeqhjmpALPeYoyjvZmZ1xM2NM8UeOmDZmCIBnKyRZ97pz5bMCjrs38WM22O7LJuw=="
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.uid2.devapp.R

@Composable
fun ActionButtonView(modifier: Modifier, onResetClick: () -> Unit, onRefreshClick: () -> Unit) {
Expand All @@ -15,11 +17,11 @@ fun ActionButtonView(modifier: Modifier, onResetClick: () -> Unit, onRefreshClic
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(onClick = onResetClick) {
Text("Reset")
Text(stringResource(id = R.string.action_reset))
}

Button(onClick = onRefreshClick) {
Text("Manual Refresh")
Text(stringResource(id = R.string.action_refresh))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,47 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.Icons.AutoMirrored.Filled
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Email
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.uid2.devapp.R

@Composable
fun EmailInputView(modifier: Modifier, onEmailEntered: (String) -> Unit) {
fun IdentityInputView(modifier: Modifier = Modifier, label: String, icon: ImageVector, onEntered: (String) -> Unit) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
val emailAddress = remember { mutableStateOf(TextFieldValue()) }
val identityData = remember { mutableStateOf(TextFieldValue()) }

TextField(
value = emailAddress.value,
onValueChange = { emailAddress.value = it },
label = { Text(stringResource(R.string.email)) },
value = identityData.value,
onValueChange = { identityData.value = it },
label = { Text(label) },
singleLine = true,
modifier = Modifier.weight(1f),
leadingIcon = {
Icon(
imageVector = Icons.Default.Email,
contentDescription = stringResource(R.string.email_icon_content_description),
imageVector = icon,
contentDescription = null,
)
},
)

FloatingActionButton(
onClick = { onEmailEntered(emailAddress.value.text) },
onClick = { onEntered(identityData.value.text) },
shape = CircleShape,
backgroundColor = MaterialTheme.colors.primary,
) {
Icon(
imageVector = Filled.ArrowForward,
contentDescription = stringResource(R.string.email_submit_content_description),
contentDescription = null,
tint = Color.White,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ fun UserIdentityView(modifier: Modifier, identity: UID2Identity?, status: Identi
.padding(0.dp, 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
modifier = Modifier.padding(bottom = 10.dp),
text = stringResource(id = R.string.current_identity),
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
)

identity?.let {
UserIdentityParameter(stringResource(R.string.identity_advertising_token), identity.advertisingToken)
UserIdentityParameter(stringResource(R.string.identity_refresh_token), identity.refreshToken)
Expand Down
9 changes: 9 additions & 0 deletions dev-app/src/main/java/com/uid2/dev/utils/ByteArrayEx.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.uid2.dev.utils

import android.util.Base64

/**
* Extension method to encode a ByteArray to a String. This uses the android.util version of Base64 to keep our minimum
* SDK low.
*/
fun ByteArray.encodeBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP)
9 changes: 7 additions & 2 deletions dev-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<string name="app_name">UID2 SDK Dev App</string>

<string name="email">Email</string>
<string name="email_icon_content_description">Email Icon</string>
<string name="email_submit_content_description">Submit Email</string>
<string name="phone">Phone Number</string>
<string name="generate_client_side">Client Side</string>

<string name="current_identity">Current Identity</string>

<string name="identity_advertising_token">Advertising Token</string>
<string name="identity_refresh_token">Refresh Token</string>
Expand All @@ -21,4 +23,7 @@
<string name="status_refresh_expired">Refresh Expired</string>
<string name="status_opt_out">OptOut</string>

<string name="action_reset">Reset</string>
<string name="action_refresh">Manual Refresh</string>

</resources>
Loading
Loading