mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-09-01 12:26:08 +08:00
Improves timezone and locale support (#106)
* Improves timezone and locale support * Fixes user controller
This commit is contained in:
parent
16e58a248c
commit
c525d4826a
13 changed files with 90 additions and 6 deletions
|
@ -1,7 +1,7 @@
|
|||
exports.up = async function(knex) {
|
||||
|
||||
|
||||
}
|
||||
|
||||
exports.down = async function(knex) {
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
exports.up = async function(knex) {
|
||||
await knex.schema
|
||||
.table('users', function(table) {
|
||||
table.string('locale').after('timezone')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema
|
||||
.table('users', function(table) {
|
||||
table.dropColumn('locale')
|
||||
})
|
||||
}
|
6
apps/platform/package-lock.json
generated
6
apps/platform/package-lock.json
generated
|
@ -39,6 +39,7 @@
|
|||
"koa-body": "5.0.0",
|
||||
"koa-jwt": "^4.0.3",
|
||||
"koa-static": "^5.0.0",
|
||||
"libphonenumber-js": "^1.10.24",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-pushnotifications": "^2.0.3",
|
||||
"node-schedule": "^2.1.0",
|
||||
|
@ -8167,6 +8168,11 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.10.24",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.24.tgz",
|
||||
"integrity": "sha512-3Dk8f5AmrcWqg+oHhmm9hwSTqpWHBdSqsHmjCJGroULFubi0+x7JEIGmRZCuL3TI8Tx39xaKqfnhsDQ4ALa/Nw=="
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"koa-body": "5.0.0",
|
||||
"koa-jwt": "^4.0.3",
|
||||
"koa-static": "^5.0.0",
|
||||
"libphonenumber-js": "^1.10.24",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-pushnotifications": "^2.0.3",
|
||||
"node-schedule": "^2.1.0",
|
||||
|
|
|
@ -13,7 +13,7 @@ export type ClientAliasParams = {
|
|||
|
||||
export type ClientIdentity = RequireAtLeastOne<ClientAliasParams, 'anonymous_id' | 'external_id'>
|
||||
|
||||
export type ClientIdentifyParams = Partial<Pick<User, 'email' | 'phone' | 'data'>> & ClientIdentity
|
||||
export type ClientIdentifyParams = Partial<Pick<User, 'email' | 'phone' | 'timezone' | 'data'>> & ClientIdentity
|
||||
|
||||
export type ClientIdentifyUser = Pick<User, 'external_id'> & Partial<Pick<User, 'email' | 'phone' | 'data'>>
|
||||
|
||||
|
@ -24,6 +24,7 @@ export type ClientDeleteUsersRequest = string[]
|
|||
export type ClientPostEvent = {
|
||||
name: string
|
||||
data?: Record<string, unknown>
|
||||
created_at?: Date
|
||||
} & ClientIdentity
|
||||
|
||||
export type ClientPostEventsRequest = ClientPostEvent[]
|
||||
|
@ -52,6 +53,7 @@ export type SegmentPostEvent = {
|
|||
traits?: Record<string, any>
|
||||
type: 'track' | 'alias' | 'identify'
|
||||
timestamp: string
|
||||
locale: string
|
||||
} & (
|
||||
{
|
||||
type: 'track',
|
||||
|
|
|
@ -64,6 +64,14 @@ const identifyParams: JSONSchemaType<ClientIdentifyParams> = {
|
|||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
|
|
|
@ -81,6 +81,8 @@ router.post('/segment', async ctx => {
|
|||
...identity,
|
||||
email: event.traits.email,
|
||||
phone: event.traits.phone,
|
||||
timezone: event.context.timezone,
|
||||
locale: event.locale,
|
||||
data: event.traits,
|
||||
},
|
||||
}))
|
||||
|
@ -92,6 +94,7 @@ router.post('/segment', async ctx => {
|
|||
...identity,
|
||||
name: event.event,
|
||||
data: { ...event.properties, ...event.context },
|
||||
created_at: new Date(event.timestamp),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { RenderContext } from '../render'
|
|||
import Template, { TemplateType } from '../render/Template'
|
||||
import { User } from '../users/User'
|
||||
import { UserEvent } from '../users/UserEvent'
|
||||
import { partialMatchLocale } from '../utilities'
|
||||
import { MessageTrigger } from './MessageTrigger'
|
||||
|
||||
interface MessageTriggerHydrated<T> {
|
||||
|
@ -34,8 +35,16 @@ export async function loadSendJob<T extends TemplateType>({ campaign_id, user_id
|
|||
qb => qb.where('campaign_id', campaign_id),
|
||||
)
|
||||
|
||||
// Determine what template to send to the user based on the following:
|
||||
// - Find an exact match of users locale with a template
|
||||
// - Find a partial match (same root locale i.e. `en` vs `en-US`)
|
||||
// - If a project locale is set and there is amtch, use that template
|
||||
// - If there is a project locale and its a partial match, use
|
||||
// - Otherwise return any template available
|
||||
const template = templates.find(item => item.locale === user.locale)
|
||||
|| templates.find(item => partialMatchLocale(item.locale, user.locale))
|
||||
|| templates.find(item => item.locale === project.locale)
|
||||
|| templates.find(item => partialMatchLocale(item.locale, project.locale))
|
||||
|| templates[0]
|
||||
|
||||
// If campaign or template dont exist, log and abort
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ClientIdentity } from '../client/Client'
|
||||
import Model, { ModelParams } from '../core/Model'
|
||||
import parsePhoneNumber from 'libphonenumber-js'
|
||||
|
||||
export interface TemplateUser extends Record<string, any> {
|
||||
id: string
|
||||
|
@ -42,7 +43,6 @@ export class User extends Model {
|
|||
phone?: string
|
||||
devices?: Device[]
|
||||
data!: Record<string, any> // first_name, last_name live in data
|
||||
attributes!: UserAttribute[] // ???
|
||||
timezone!: string
|
||||
locale!: string
|
||||
|
||||
|
@ -80,6 +80,19 @@ export class User extends Model {
|
|||
get lastName() {
|
||||
return this.data.last_name ?? this.data.lastName
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const json = super.toJSON()
|
||||
|
||||
if (this.phone) {
|
||||
const parsedNumber = parsePhoneNumber(this.phone)
|
||||
if (parsedNumber) {
|
||||
json.phone = parsedNumber.formatInternational()
|
||||
}
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
export type UserParams = Partial<Pick<User, 'email' | 'phone' | 'data'>> & ClientIdentity
|
||||
export type UserParams = Partial<Pick<User, 'email' | 'phone' | 'timezone' |'locale' | 'data'>> & ClientIdentity
|
||||
|
|
|
@ -48,6 +48,14 @@ const patchUsersRequest: JSONSchemaType<UserParams[]> = {
|
|||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
|
@ -74,6 +82,14 @@ const patchUsersRequest: JSONSchemaType<UserParams[]> = {
|
|||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
|
|
|
@ -84,6 +84,17 @@ export const batch = <T>(arr: T[], size: number) => {
|
|||
return result
|
||||
}
|
||||
|
||||
export const parseLocale = (locale: string): string | undefined => {
|
||||
const parts = locale.split('-')
|
||||
return parts.length === 1 ? locale[0] : locale
|
||||
}
|
||||
|
||||
export const partialMatchLocale = (locale1?: string, locale2?: string) => {
|
||||
const locale1Root = locale1?.split('-')[0]
|
||||
const locale2Root = locale2?.split('-')[0]
|
||||
return locale1 === locale2 || locale1Root === locale2Root
|
||||
}
|
||||
|
||||
export function extractQueryParams<T extends Record<string, any>>(search: URLSearchParams | Record<string, undefined | string | string[]>, schema: JSONSchemaType<T>) {
|
||||
return validate(schema, Object.entries<JSONSchemaType<any>>(schema.properties).reduce((a, [name, def]) => {
|
||||
let values: string[]
|
||||
|
|
|
@ -36,7 +36,7 @@ export default function UserDetailEvents() {
|
|||
}}
|
||||
/>
|
||||
<Modal title={event?.name}
|
||||
size="regular"
|
||||
size="large"
|
||||
open={event != null}
|
||||
onClose={() => setEvent(undefined)}
|
||||
>
|
||||
|
|
|
@ -17,6 +17,8 @@ export default function UserTabs() {
|
|||
{ key: 'full_name', title: 'Name' },
|
||||
{ key: 'email' },
|
||||
{ key: 'phone' },
|
||||
{ key: 'timezone' },
|
||||
{ key: 'locale' },
|
||||
{ key: 'created_at' },
|
||||
{ key: 'updated_at' },
|
||||
]}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue