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

feat(sms-limiting): admin facing frontend #2280

Merged
merged 23 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
651e0ab
feat(configure-mobile.client.directive): updated modal to use new mes…
seaerchin Jun 14, 2021
dbc0126
refactor(verification.constants): shifted sms_verification_limit to s…
seaerchin Jun 14, 2021
380fa4b
feat(configure-mobile.client): added frontend ui for toggling on/off
seaerchin Jun 14, 2021
4728762
feat(configure-mobile.client.view): added lock and disabled state whe…
seaerchin Jun 22, 2021
5e958c3
refactor(configure-mobile.client.view): changed text message to be er…
seaerchin Jun 22, 2021
bfdd11f
refactor(edit-form.css): added inline-padding for error message display
seaerchin Jun 22, 2021
809a891
fix(configure-mobile.client.view): updated form twilio account link
seaerchin Jun 22, 2021
92f8a3a
feat(configure-mobile.client.directive): added loading state for toggle
seaerchin Jun 22, 2021
5d32430
refactor(configure-mobile.client.view): update wording for error message
seaerchin Jun 23, 2021
179c70c
fix(configure-mobile.client.view.html): added ng-show so that error o…
seaerchin Jun 23, 2021
294024a
feat(public/services): adds new smsservice
seaerchin Jun 30, 2021
985c1d6
feat(configure-mobile.client.directive): connected sms counts to fetc…
seaerchin Jun 30, 2021
df66edd
refactor(pop-up-modal): adds variable to check if we should display r…
seaerchin Jul 1, 2021
971984a
test(smsservice): adds tests for sms service
seaerchin Jul 1, 2021
1bc1033
refactor(configure-mobile.client): adds error reporting and handling …
seaerchin Jul 1, 2021
bd33e1c
refactor(smsservice): renamed SmsService to AdminMetaService
seaerchin Jul 2, 2021
722b333
refactor(adminmetaservice): renamed method for greater clarity
seaerchin Jul 2, 2021
35e984b
chore(configure-mobile.client): addressed PR comments
seaerchin Jul 5, 2021
093d9e4
refactor(configure-mobile.client): uses env var instead of using cons…
seaerchin Jul 7, 2021
2d4491f
refactor(mail.service): changes to use env var instead of const
seaerchin Jul 7, 2021
5869bed
refactor(adminmetaservice): updated definitions to fit BE
seaerchin Jul 7, 2021
d90e441
refactor(configure-mobile.client): changed condition to be same as BE
seaerchin Jul 14, 2021
cf5ff26
refactor(configure-mobile.client.directive): extracts formatting to o…
seaerchin Jul 14, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function PopUpModalController($uibModalInstance, externalScope) {
vm.description = externalScope.description
vm.confirmButtonText = externalScope.confirmButtonText
vm.cancelButtonText = externalScope.cancelButtonText
vm.isImportant = externalScope.isImportant
vm.cancel = $uibModalInstance.close

vm.confirm = function () {
Expand Down
11 changes: 11 additions & 0 deletions src/public/modules/forms/admin/css/edit-form.css
Original file line number Diff line number Diff line change
Expand Up @@ -1426,3 +1426,14 @@ a.modal-cancel-btn:hover {
.pull-right {
cursor: pointer;
}

.inline-padding {
/* margin is set so that the error message appears to be in the same block as the toggle options */
margin: 0 15px;
}

/* Spinner */
.loading-spinner {
display: flex;
justify-content: flex-end;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<div class="row">
<div class="toggle-option">
<div class="col-xs-8 label-custom label-medium label-bottom">
<div
ng-class="{'toggle-option--disabled' : field.hasAdminExceededSmsLimit || field.hasRetrievalError}"
class="col-xs-8 label-custom label-medium label-bottom"
>
OTP verification
<i
class="glyphicon glyphicon-question-sign"
Expand All @@ -9,9 +12,13 @@
></i>
</div>
<div class="col-xs-4 field-input">
<div ng-show="isLoading" class="loading-spinner">
seaerchin marked this conversation as resolved.
Show resolved Hide resolved
<i class="bx bx-loader bx-spin bx-lg icon-spacing"></i>
</div>
<label
ng-show="!isLoading"
class="toggle-selector pull-right"
ng-class="field.isVerifiable? 'toggle-selector-on' : ''"
ng-class="{'toggle-selector-on': field.isVerifiable && !field.hasAdminExceededSmsLimit && !field.hasRetrievalError, 'toggle-selector--disabled': field.hasAdminExceededSmsLimit || field.hasRetrievalError}"
onclick=""
>
<input
Expand All @@ -20,10 +27,30 @@
ng-click="openVerifiedSMSModal()"
/>
<div class="toggle-selector-switch">
<i ng-class="field.isVerifiable ? 'bx bx-check' : 'bx bx-x' "></i>
<i
ng-show="!field.hasAdminExceededSmsLimit && !field.hasRetrievalError"
ng-class="field.isVerifiable ? 'bx bx-check' : 'bx bx-x'"
></i>
<i
ng-show="field.hasAdminExceededSmsLimit || field.hasRetrievalError"
class="bx bxs-lock"
></i>
</div>
</label>
</div>
</div>
<br />
<div
class="alert-custom alert-error inline-padding"
ng-show="field.hasAdminExceededSmsLimit"
>
<i class="bx bx-exclamation bx-md icon-spacing"></i>
<span class="alert-msg">
You have reached the free tier limit for SMS verification. To continue
using SMS verification,
<a ng-href="{{verifiedSmsSetupLink}}" target="_blank">
please arrange for billing with us
</a></span
>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
'use strict'
const { get } = require('lodash')

const {
ADMIN_VERIFIED_SMS_STATES,
} = require('../../../../../shared/util/verification')

const AdminMetaService = require('../../../../services/AdminMetaService')

const { injectedVariables } = require('../../../../utils/injectedVariables')

angular
.module('forms')
Expand All @@ -10,24 +19,89 @@ function configureMobileDirective() {
'modules/forms/admin/directiveViews/configure-mobile.client.view.html',
restrict: 'E',
scope: {
field: '=',
field: '<',
form: '<',
name: '=',
characterLimit: '=',
isLoading: '<',
},
controller: [
'$q',
'$uibModal',
'$scope',
'$translate',
function ($uibModal, $scope, $translate) {
// Get support form link from translation json.
$translate('LINKS.SUPPORT_FORM_LINK').then((supportFormLink) => {
$scope.supportFormLink = supportFormLink
})
'Toastr',
function ($q, $uibModal, $scope, $translate, Toastr) {
// Get the link for onboarding the form from the translation json
$translate('LINKS.VERIFIED_SMS_SETUP_LINK').then(
(verifiedSmsSetupLink) => {
$scope.verifiedSmsSetupLink = verifiedSmsSetupLink
},
)

// Formats a given string as a number by setting it to US locale.
// Concretely, this adds commas between every thousand.
const formatStringAsNumber = (num) =>
Number(num).toLocaleString('en-US')

// NOTE: This is set on scope as it is used by the UI to determine if the toggle is loading
$scope.isLoading = true
$scope.field.hasRetrievalError = false

const formattedSmsVerificationLimit =
// Format so that it has commas; conversion is required because it's string initially
formatStringAsNumber(injectedVariables.smsVerificationLimit)

const getAdminVerifiedSmsState = (verifiedSmsCount, msgSrvcId) => {
if (msgSrvcId) {
return ADMIN_VERIFIED_SMS_STATES.hasMessageServiceId
}
if (verifiedSmsCount <= injectedVariables.smsVerificationLimit) {
return ADMIN_VERIFIED_SMS_STATES.belowLimit
}
return ADMIN_VERIFIED_SMS_STATES.limitExceeded
}

$q.when(
AdminMetaService.getFreeSmsCountsUsedByFormAdmin($scope.form._id),
)
.then((smsCounts) => {
$scope.verifiedSmsCount = smsCounts
$scope.adminVerifiedSmsState = getAdminVerifiedSmsState(
smsCounts,
$scope.form.msgSrvcName,
)
// NOTE: This links into the verifiable field component and hence, is used by both email and mobile
$scope.field.hasAdminExceededSmsLimit =
$scope.adminVerifiedSmsState ===
ADMIN_VERIFIED_SMS_STATES.limitExceeded
})
.catch((error) => {
$scope.field.hasRetrievalError = true
Toastr.error(
get(
error,
'response.data.message',
'Sorry, an error occurred. Please refresh the page to toggle OTP verification.',
),
)
})
.finally(() => ($scope.isLoading = false))

// Only open if the admin has sms counts below the limit.
// If the admin has counts above limit without a message id, the toggle should be disabled anyway.
// Otherwise, if the admin has a message id, just enable it without the modal
$scope.openVerifiedSMSModal = function () {
const isTogglingOnVerifiedSms = !$scope.field.isVerifiable
$scope.verifiedSMSModal =
const isAdminBelowLimit =
$scope.adminVerifiedSmsState ===
ADMIN_VERIFIED_SMS_STATES.belowLimit
const shouldShowModal =
isTogglingOnVerifiedSms &&
isAdminBelowLimit &&
!$scope.field.hasRetrievalError
$scope.verifiedSMSModal =
shouldShowModal &&
$uibModal.open({
animation: true,
backdrop: 'static',
Expand All @@ -39,14 +113,22 @@ function configureMobileDirective() {
resolve: {
externalScope: function () {
return {
title: 'Verified SMS charges',
confirmButtonText: 'OK, Noted',
title: `OTP verification will be disabled at ${formattedSmsVerificationLimit} responses`,
confirmButtonText: 'Accept',
description: `
Under 10,000 form responses: Free verified SMS
<br><br>
Above 10,000 form responses: <b>~US$0.0395 per SMS - <a href=${$scope.supportFormLink} target="_blank" class="">contact us</a>
for billing</b>. Forms exceeding the free tier without billing will be deactivated.
We provide SMS OTP verification for free up to ${formattedSmsVerificationLimit} responses. OTP verification will be automatically disabled when your account reaches ${formattedSmsVerificationLimit} responses.
<br></br>
If you require OTP verification for more than ${formattedSmsVerificationLimit} responses,
<a href=${
$scope.verifiedSmsSetupLink
} target="_blank" class=""> please arrange advance billing with us. </a>

<br></br>
<small>Current response count: ${formatStringAsNumber(
$scope.verifiedSmsCount,
)}/${formattedSmsVerificationLimit}</small>
`,
isImportant: true,
}
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,7 @@ <h2 class="modal-title">
<configure-mobile-directive
ng-if="vm.field.fieldType === 'mobile'"
field="vm.field"
form="vm.myform"
name="'SMS'"
character-limit="300"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
<button
type="submit"
ng-click="vm.confirm()"
class="btn-custom btn-medium modal-save-btn"
ng-class="vm.isImportant ? 'red-bg-dark' : 'modal-save-btn'"
class="btn-custom btn-medium"
>
{{ vm.confirmButtonText || 'OK' }}
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<ng-transclude
ng-class="(vm.field.isVerifiable ? 'col-sm-9' : 'col-sm-12') + ' vfn-slot'"
></ng-transclude>
<div class="verifiable" ng-if="vm.field.isVerifiable">
<div
class="verifiable"
ng-if="vm.field.fieldType === 'email' ? vm.field.isVerifiable : vm.field.isVerifiable && !vm.field.hasAdminExceededSmsLimit && !vm.field.hasRetrievalError"
>
<div class="vfn-btn-container col-sm-3 col-xs-12 pull-right field-group">
<!-- Verify button -->
<button
Expand Down
16 changes: 16 additions & 0 deletions src/public/services/AdminMetaService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from 'axios'

/** Exported for testing */
export const FORM_API_PREFIX = '/api/v3/admin/forms'

/**
* Retrieves the free sms counts used by the admin of a specified form
* @param formId
* @returns The amount of free sms counts used by the admin of the form
*/
export const getFreeSmsCountsUsedByFormAdmin = (
formId: string,
): Promise<number> =>
axios
.get<number>(`${FORM_API_PREFIX}/${formId}/verified-sms/count/free`)
.then(({ data }) => data)
29 changes: 29 additions & 0 deletions src/public/services/__tests__/AdminMetaService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import axios from 'axios'
import { ObjectId } from 'bson-ext'
import { mocked } from 'ts-jest/utils'

import * as AdminMetaService from '../AdminMetaService'

jest.mock('axios')
const MockAxios = mocked(axios, true)

describe('AdminMetaService', () => {
describe('getFreeSmsCountsUsedByFormAdmin', () => {
const MOCK_FORM_ID = new ObjectId().toHexString()
it('should call the endpoint successfully when parameters are provided', async () => {
// Arrange
MockAxios.get.mockResolvedValueOnce({ data: 'some data' })
// Act
const actual = await AdminMetaService.getFreeSmsCountsUsedByFormAdmin(
MOCK_FORM_ID,
)

// Assert
expect(MockAxios.get).toBeCalledWith(
`${AdminMetaService.FORM_API_PREFIX}/${MOCK_FORM_ID}/verified-sms/count/free`,
)
expect(actual).toBe('some data')
})
})
})
1 change: 1 addition & 0 deletions src/public/translations/en-SG/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"GO_GOV": "https://go.gov.sg",
"POSTMAN_GOV": "https://postman.gov.sg",
"SUPPORT_FORM_LINK": "https://go.gov.sg/formsg-support",
"VERIFIED_SMS_SETUP_LINK": "https://go.gov.sg/form-twilio-setup",
"WHITELISTED_ATTACHMENT_TYPES": "https://go.gov.sg/formsg-cwl",
"SINGPASS_ELIGIBILITY_FAQ": "https://www.ifaq.gov.sg/SINGPASS/apps/Fcd_faqmain.aspx#FAQ_2101385",
"ESERVICE_ID_FAQ": "https://go.gov.sg/formsg-spcp",
Expand Down
2 changes: 2 additions & 0 deletions src/public/utils/injectedVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface FrontendInjectedVariables {
isCPMaintenance: string | null
GATrackingID: string | null
spcpCookieDomain: string | null
smsVerificationLimit: number
}

// NOTE: As these variables are not injected until runtime
Expand All @@ -33,4 +34,5 @@ export const injectedVariables: FrontendInjectedVariables = {
isCPMaintenance: formsgWindow.isCPMaintenance ?? null,
GATrackingID: formsgWindow.GATrackingID ?? null,
spcpCookieDomain: formsgWindow.spcpCookieDomain ?? null,
smsVerificationLimit: formsgWindow.smsVerificationLimit,
}
6 changes: 6 additions & 0 deletions src/shared/util/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ export enum VfnErrors {
TransactionNotFound = 'TRANSACTION_NOT_FOUND',
InvalidMobileNumber = 'INVALID_MOBILE_NUMBER',
}

export enum ADMIN_VERIFIED_SMS_STATES {
limitExceeded = 'LIMIT_EXCEEDED',
belowLimit = 'BELOW_LIMIT',
hasMessageServiceId = 'MESSAGE_SERVICE_ID_OBTAINED',
}