Skip to content

Commit

Permalink
feat: Ordering for challenge list
Browse files Browse the repository at this point in the history
  • Loading branch information
yafuquen authored Sep 7, 2021
1 parent 93d1b4b commit 790fb17
Show file tree
Hide file tree
Showing 18 changed files with 173 additions and 57 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ In order to run the sample app, you have to create a project and application in
* Press Create factor
* Copy the factor Sid

### Sending a challenge
### Sending and updating a challenge
* Go to Create Push Challenge page (/challenge path in your sample backend)
* Enter the `identity` you used in factor creation
* Enter the `Factor Sid` you added
Expand All @@ -131,6 +131,12 @@ In order to run the sample app, you have to create a project and application in
* Approve or deny the challenge
* After the challenge is updated, you will see the challenge status in the backend's `Create Push Challenge` view

#### Silently approve challenges

You can enable the option "Silently approve challenges" for a factor. After enabling it, every challenge received as a push notification for that factor will be silently approved, so user interaction is not required. The option will be saved for the session, so to test it for background, enable the option and send the app to background by pressing the home button or opening another app.

You can silently approve challenges when your app already knows that the user is trying to login on the same device as the registered device that is being challenged.

<a name='Logging'></a>

## Logging
Expand Down
21 changes: 21 additions & 0 deletions sample/src/main/java/com/twilio/verify/sample/model/AppModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2021 Twilio Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.twilio.verify.sample.model

object AppModel {
val silentlyApproveChallengesPerFactor = mutableMapOf<String, Boolean>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.os.bundleOf
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.twilio.verify.models.ChallengeStatus.Approved
import com.twilio.verify.models.UpdatePushChallengePayload
import com.twilio.verify.sample.R
import com.twilio.verify.sample.TwilioVerifyAdapter
import com.twilio.verify.sample.model.AppModel
import com.twilio.verify.sample.view.MainActivity
import com.twilio.verify.sample.view.challenges.update.ARG_CHALLENGE_SID
import com.twilio.verify.sample.view.challenges.update.ARG_FACTOR_SID
Expand Down Expand Up @@ -66,44 +71,80 @@ class FirebasePushService() : FirebaseMessagingService() {
val challengeSid = bundle.getString(challengeSidKey)
val message = bundle.getString(messageKey)
if (factorSid != null && challengeSid != null) {
twilioVerifyAdapter.showChallenge(challengeSid, factorSid)
message?.let {
if (AppModel.silentlyApproveChallengesPerFactor[factorSid] == true) {
approveChallenge(challengeSid, factorSid, message)
} else {
showChallenge(challengeSid, factorSid, message)
}
}
}

private fun approveChallenge(
challengeSid: String,
factorSid: String,
message: String?
) {
twilioVerifyAdapter.updateChallenge(
UpdatePushChallengePayload(
factorSid,
challengeSid,
Approved
),
{
showChallenge(challengeSid, factorSid, message, true)
},
{
it.printStackTrace()
}
)
}

private fun showChallenge(
challengeSid: String,
factorSid: String,
message: String?,
approved: Boolean = false
) {
twilioVerifyAdapter.showChallenge(challengeSid, factorSid)
val notificationMessage =
if (approved) getString(R.string.silent_approved_challenge) else message
notificationMessage?.let {
if (VERSION.SDK_INT >= VERSION_CODES.O) {
createNotificationChannel()
val i = Intent(this, MainActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
i.putExtras(bundleOf(ARG_FACTOR_SID to factorSid, ARG_CHALLENGE_SID to challengeSid))
val pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_ONE_SHOT)
val builder = NotificationCompat.Builder(
this,
channelId
)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_challenge)
.setContentTitle(getString(R.string.new_challenge))
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
with(NotificationManagerCompat.from(this)) {
notify(challengeSid.hashCode(), builder.build())
}
}
val i = Intent(this, MainActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
i.putExtras(bundleOf(ARG_FACTOR_SID to factorSid, ARG_CHALLENGE_SID to challengeSid))
val pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_ONE_SHOT)
val builder = NotificationCompat.Builder(
this,
channelId
)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_challenge)
.setContentTitle(getString(R.string.new_challenge))
.setContentText(notificationMessage)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
with(NotificationManagerCompat.from(this)) {
notify(challengeSid.hashCode(), builder.build())
}
}
}

@RequiresApi(VERSION_CODES.O)
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = getString(R.string.channel_name)
val descriptionText = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(channelId, name, importance).apply {
description = descriptionText
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val name = getString(R.string.channel_name)
val descriptionText = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(channelId, name, importance).apply {
description = descriptionText
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}

private fun getBundleFromMessage(remoteMessage: RemoteMessage?): Bundle {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.twilio.verify.models.Challenge
import com.twilio.verify.sample.R
import com.twilio.verify.sample.R.layout
import com.twilio.verify.sample.model.AppModel
import com.twilio.verify.sample.view.challenges.update.ARG_CHALLENGE_SID
import com.twilio.verify.sample.view.challenges.update.ARG_FACTOR_SID
import com.twilio.verify.sample.view.showError
Expand All @@ -40,6 +41,7 @@ import com.twilio.verify.sample.viewmodel.Factor
import com.twilio.verify.sample.viewmodel.FactorError
import com.twilio.verify.sample.viewmodel.FactorViewModel
import kotlinx.android.synthetic.main.fragment_factor_challenges.challenges
import kotlinx.android.synthetic.main.fragment_factor_challenges.silentApproveCheck
import kotlinx.android.synthetic.main.fragment_factors.content
import kotlinx.android.synthetic.main.view_factor.factorNameText
import kotlinx.android.synthetic.main.view_factor.factorSidText
Expand Down Expand Up @@ -99,6 +101,10 @@ class FactorChallengesFragment : Fragment() {
challenges.addItemDecoration(dividerItemDecoration)
factorViewModel.loadFactor(sid)
challengesViewModel.loadChallenges(sid)
silentApproveCheck.isChecked = AppModel.silentlyApproveChallengesPerFactor[sid] == true
silentApproveCheck.setOnCheckedChangeListener { _, isChecked ->
factorViewModel.changeSilentApproveChallenges(sid, isChecked)
}
}

private fun showFactor(factor: com.twilio.verify.models.Factor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.twilio.verify.models.Challenge
import com.twilio.verify.models.ChallengeListOrder.Desc
import com.twilio.verify.models.ChallengeListPayload
import com.twilio.verify.sample.TwilioVerifyAdapter

Expand All @@ -30,10 +31,9 @@ class ChallengesViewModel(private val twilioVerifyAdapter: TwilioVerifyAdapter)

fun loadChallenges(factorSid: String) {
twilioVerifyAdapter.getAllChallenges(
ChallengeListPayload(factorSid, PAGE_SIZE),
ChallengeListPayload(factorSid, PAGE_SIZE, order = Desc),
{ challengeList ->
challenges.value =
ChallengeList(challengeList.challenges.sortedByDescending { it.createdAt })
challenges.value = ChallengeList(challengeList.challenges)
},
{
challenges.value = ChallengesError(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.twilio.verify.models.Factor
import com.twilio.verify.sample.TwilioVerifyAdapter
import com.twilio.verify.sample.model.AppModel
import com.twilio.verify.sample.model.CreateFactorData
import com.twilio.verify.sample.networking.backendAPIClient

Expand Down Expand Up @@ -54,6 +55,10 @@ class FactorViewModel(private val twilioVerifyAdapter: TwilioVerifyAdapter) : Vi
}
)
}

fun changeSilentApproveChallenges(sid: String, approveSilently: Boolean) {
AppModel.silentlyApproveChallengesPerFactor[sid] = approveSilently
}
}

sealed class FactorResult
Expand Down
12 changes: 11 additions & 1 deletion sample/src/main/res/layout/fragment_factor_challenges.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
app:layout_constraintTop_toTopOf="parent"
/>

<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/silentApproveCheck"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/silent_approve"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/factor"
/>
<TextView
android:id="@+id/challengesLabel"
style="@style/TextAppearance.AppCompat.Subhead"
Expand All @@ -43,7 +53,7 @@
android:text="@string/challenges"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/factor"
app:layout_constraintTop_toBottomOf="@id/silentApproveCheck"
/>

<androidx.recyclerview.widget.RecyclerView
Expand Down
2 changes: 2 additions & 0 deletions sample/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@
<string name="challenge_status">Status:</string>
<string name="challenge_created_at">Created at:</string>
<string name="challenge_expire_on">Expire on:</string>
<string name="silent_approve">Silently approve challenges</string>
<string name="silent_approved_challenge">Challenge silently approved</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.twilio.verify.TwilioVerifyException.ErrorCode.NetworkError
import com.twilio.verify.data.DateAdapter
import com.twilio.verify.data.DateProvider
import com.twilio.verify.domain.challenge.models.FactorChallenge
import com.twilio.verify.models.ChallengeListOrder
import com.twilio.verify.models.Factor
import com.twilio.verify.networking.Authentication
import com.twilio.verify.networking.BasicAuthorization
Expand All @@ -41,6 +42,7 @@ internal const val challengeSidPath = "{ChallengeSid}"
internal const val statusParameter = "Status"
internal const val pageSizeParameter = "PageSize"
internal const val pageTokenParameter = "PageToken"
internal const val orderParameter = "Order"
internal const val signatureFieldsHeader = "Twilio-Verify-Signature-Fields"
internal const val updateChallengeURL =
"Services/$SERVICE_SID_PATH/Entities/$IDENTITY_PATH/Challenges/$challengeSidPath"
Expand Down Expand Up @@ -146,6 +148,7 @@ internal class ChallengeAPIClient(
factor: Factor,
status: String?,
pageSize: Int,
order: ChallengeListOrder,
pageToken: String?,
success: (response: JSONObject) -> Unit,
error: (TwilioVerifyException) -> Unit
Expand All @@ -157,7 +160,8 @@ internal class ChallengeAPIClient(
RequestHelper(context, BasicAuthorization(AUTHENTICATION_USER, authToken))
val queryParameters = mutableMapOf<String, Any>(
pageSizeParameter to pageSize,
FACTOR_SID_KEY to factor.sid
FACTOR_SID_KEY to factor.sid,
orderParameter to order.name.toLowerCase()
)
status?.let {
queryParameters.put(statusParameter, it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ internal class ChallengeFacade(
{ factor ->
execute(success, error) { onSuccess, onError ->
repository.getAll(
factor, challengeListPayload.status, challengeListPayload.pageSize,
factor, challengeListPayload.status, challengeListPayload.pageSize, challengeListPayload.order,
challengeListPayload.pageToken,
{ list ->
onSuccess(list)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.twilio.verify.domain.challenge
import com.twilio.verify.TwilioVerifyException
import com.twilio.verify.models.Challenge
import com.twilio.verify.models.ChallengeList
import com.twilio.verify.models.ChallengeListOrder
import com.twilio.verify.models.ChallengeStatus
import com.twilio.verify.models.Factor

Expand All @@ -41,6 +42,7 @@ internal interface ChallengeProvider {
factor: Factor,
status: ChallengeStatus?,
pageSize: Int,
order: ChallengeListOrder,
pageToken: String?,
success: (ChallengeList) -> Unit,
error: (TwilioVerifyException) -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.twilio.verify.api.ChallengeAPIClient
import com.twilio.verify.domain.challenge.models.FactorChallenge
import com.twilio.verify.models.Challenge
import com.twilio.verify.models.ChallengeList
import com.twilio.verify.models.ChallengeListOrder
import com.twilio.verify.models.ChallengeStatus
import com.twilio.verify.models.ChallengeStatus.Pending
import com.twilio.verify.models.Factor
Expand Down Expand Up @@ -97,6 +98,7 @@ internal class ChallengeRepository(
factor: Factor,
status: ChallengeStatus?,
pageSize: Int,
order: ChallengeListOrder,
pageToken: String?,
success: (ChallengeList) -> Unit,
error: (TwilioVerifyException) -> Unit
Expand All @@ -109,7 +111,7 @@ internal class ChallengeRepository(
error(e)
}
}
apiClient.getAll(factor, status?.value, pageSize, pageToken, ::toResponse, error)
apiClient.getAll(factor, status?.value, pageSize, order, pageToken, ::toResponse, error)
}

private fun toFactorChallenge(challenge: Challenge) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.twilio.verify.models

import com.twilio.verify.models.ChallengeListOrder.Asc

/**
* Describes the information required to fetch a **ChallengeList**
*/
Expand All @@ -32,8 +34,17 @@ class ChallengeListPayload(
* Status to filter the Challenges, if nothing is sent, Challenges of all status will be returned
*/
val status: ChallengeStatus? = null,
/**
* Sort challenges in order by creation date of the challenge
*/
val order: ChallengeListOrder = Asc,
/**
* Token used to retrieve the next page in the pagination arrangement
*/
val pageToken: String? = null
)

enum class ChallengeListOrder {
Asc,
Desc
}
2 changes: 1 addition & 1 deletion verify/src/test/java/com/twilio/verify/TwilioVerifyTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ class TwilioVerifyTest {
fun `Get all challenges should call success`() {
val factorSid = "factorSid123"
createFactor(factorSid, Verified)
val challengeListPayload = ChallengeListPayload(factorSid, 1, null, null)
val challengeListPayload = ChallengeListPayload(factorSid, 1, null, pageToken = null)
val expectedChallenges = JSONArray(
listOf(
challengeJSONObject("sid123", factorSid),
Expand Down
Loading

0 comments on commit 790fb17

Please sign in to comment.