@react-native-kakao/user 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.web.ts CHANGED
@@ -1,19 +1,31 @@
1
1
  import { camelCaseObject, filterNonNullishKeys, is } from '@mj-studio/js-util';
2
- import { kFetchFormUrlEncoded, kGlobalStorage, kRunWebAPI } from '@react-native-kakao/core';
2
+ import {
3
+ kAssert,
4
+ kCreateWebError,
5
+ kFetch,
6
+ kFetchFormUrlEncoded,
7
+ kGlobalStorage,
8
+ kRunWebAPI,
9
+ } from '@react-native-kakao/core';
3
10
 
4
11
  import {
5
12
  type KakaoLoginToken,
6
13
  type KakaoScopeInfo,
14
+ type KakaoServiceTerms,
7
15
  type KakaoShippingAddressResult,
8
16
  type KakaoUser as KakaoUserProfile,
9
17
  type KakaoUserAPI,
10
18
  } from './index';
19
+ import { validateShippingAddressOptions } from './validateShippingAddressOptions';
11
20
 
12
21
  declare const Kakao: {
22
+ isInitialized: () => boolean;
13
23
  Auth: {
14
24
  authorize: Function;
15
25
  logout: Function;
16
26
  setAccessToken: Function;
27
+ selectShippingAddress: () => Promise<{ address_id: number }>;
28
+ getAccessToken: () => string | null;
17
29
  };
18
30
  API: { request: Function };
19
31
  };
@@ -27,6 +39,22 @@ function isoToUnix(value?: string | number) {
27
39
  }
28
40
 
29
41
  const KakaoUser: KakaoUserAPI = {
42
+ loginWithKakao: () =>
43
+ kRunWebAPI(() => {
44
+ throw {
45
+ code: 'Package-UnsupportedPlatform',
46
+ message:
47
+ 'loginWithKakao is supported on Android and iOS. Use login with web.redirectUri on web.',
48
+ };
49
+ }),
50
+ refreshToken: () =>
51
+ kRunWebAPI(() => {
52
+ throw {
53
+ code: 'Package-UnsupportedPlatform',
54
+ message: 'The Kakao JavaScript SDK does not support refreshToken.',
55
+ };
56
+ }),
57
+ hasToken: () => kRunWebAPI(() => Kakao.Auth.getAccessToken() != null),
30
58
  getAccessToken: () =>
31
59
  kRunWebAPI(async () => {
32
60
  const { id, expires_in, app_id }: { id: number; expires_in: number; app_id: number } =
@@ -38,7 +66,62 @@ const KakaoUser: KakaoUserAPI = {
38
66
  appId: app_id,
39
67
  };
40
68
  }),
41
- issueAccessTokenWithCodeWeb: ({ clientSecret, code, redirectUri }) =>
69
+ issueAccessTokenWithCode: ({ code, codeVerifier, redirectUri, web }) =>
70
+ kRunWebAPI(async () => {
71
+ kAssert(
72
+ is.string(code) && code.length > 0 && is.string(redirectUri) && redirectUri.length > 0,
73
+ '[issueAccessTokenWithCode] code and redirectUri are required on web',
74
+ );
75
+
76
+ if (typeof Kakao === 'undefined' || !Kakao.isInitialized()) {
77
+ kCreateWebError({
78
+ code: 'Package-SDKNotInitialized',
79
+ message: 'Initialize the Kakao SDK before exchanging an authorization code.',
80
+ });
81
+ }
82
+
83
+ const token = await KakaoUser.issueAccessTokenWithCodeWeb({
84
+ code,
85
+ codeVerifier,
86
+ redirectUri: redirectUri!,
87
+ clientSecret: web?.clientSecret,
88
+ });
89
+
90
+ if (
91
+ !is.string(token.accessToken) ||
92
+ !token.accessToken ||
93
+ !is.string(token.refreshToken) ||
94
+ !token.refreshToken ||
95
+ !is.string(token.tokenType) ||
96
+ (token.idToken != null && !is.string(token.idToken)) ||
97
+ !Number.isFinite(token.expiresIn) ||
98
+ token.expiresIn < 0 ||
99
+ !Number.isFinite(token.refreshTokenExpiresIn) ||
100
+ token.refreshTokenExpiresIn < 0 ||
101
+ (token.scope != null && !is.string(token.scope))
102
+ ) {
103
+ kCreateWebError({
104
+ code: 'Package-APIResponseNotFound',
105
+ message: 'issueAccessTokenWithCode returned invalid token data.',
106
+ });
107
+ }
108
+
109
+ const issuedAt = Math.floor(Date.now() / 1000);
110
+ KakaoUser.setAccessTokenWeb(token.accessToken);
111
+
112
+ return {
113
+ accessToken: token.accessToken,
114
+ refreshToken: token.refreshToken,
115
+ tokenType: token.tokenType,
116
+ idToken: token.idToken,
117
+ accessTokenExpiresIn: token.expiresIn,
118
+ refreshTokenExpiresIn: token.refreshTokenExpiresIn,
119
+ accessTokenExpiresAt: issuedAt + token.expiresIn,
120
+ refreshTokenExpiresAt: issuedAt + token.refreshTokenExpiresIn,
121
+ scopes: token.scope?.split(' ').filter(Boolean) ?? [],
122
+ };
123
+ }),
124
+ issueAccessTokenWithCodeWeb: ({ clientSecret, code, codeVerifier, redirectUri }) =>
42
125
  kRunWebAPI(async () => {
43
126
  const res = await kFetchFormUrlEncoded<{
44
127
  token_type: string;
@@ -55,6 +138,7 @@ const KakaoUser: KakaoUserAPI = {
55
138
  client_id: kGlobalStorage.restApiKey,
56
139
  redirect_uri: redirectUri,
57
140
  code,
141
+ code_verifier: codeVerifier,
58
142
  client_secret: clientSecret,
59
143
  }),
60
144
  }).then((r) => r.body);
@@ -69,10 +153,12 @@ const KakaoUser: KakaoUserAPI = {
69
153
  kRunWebAPI(() => {
70
154
  const { loginHint, nonce, prompt, redirectUri, scope, state, throughTalk, serviceTerms } =
71
155
  params?.web ?? {};
156
+ const channelPublicIds = params?.channelPublicIds;
72
157
 
73
158
  return Kakao.Auth.authorize(
74
159
  filterNonNullishKeys({
75
- loginHint,
160
+ loginHint: params?.loginHint ?? loginHint,
161
+ channelPublicId: channelPublicIds?.length ? channelPublicIds.join(',') : undefined,
76
162
  nonce: params?.nonce ?? nonce,
77
163
  prompt: (
78
164
  prompt ??
@@ -88,6 +174,13 @@ const KakaoUser: KakaoUserAPI = {
88
174
  }),
89
175
  );
90
176
  }),
177
+ signup: ({ properties } = {}) =>
178
+ kRunWebAPI(async () => {
179
+ await Kakao.API.request({
180
+ url: '/v1/user/signup',
181
+ data: filterNonNullishKeys({ properties }),
182
+ });
183
+ }),
91
184
  logout: () =>
92
185
  kRunWebAPI(async () => {
93
186
  try {
@@ -119,42 +212,78 @@ const KakaoUser: KakaoUserAPI = {
119
212
  data: scopes?.length ? { scopes } : undefined,
120
213
  });
121
214
 
122
- return camelCaseObject(response.scopes) as KakaoScopeInfo[];
215
+ return camelCaseObject(response.scopes ?? []) as KakaoScopeInfo[];
123
216
  }),
124
- revokeScopes: (scopes: string[]) =>
217
+ revokeScopes: async (scopes: string[]) => {
218
+ await revokeScopesWithResult(scopes);
219
+ },
220
+ revokeScopesWithResult,
221
+ serviceTerms: ({ result, tags } = {}) =>
125
222
  kRunWebAPI(async () => {
126
- await Kakao.API.request({
127
- url: '/v2/user/revoke/scopes',
128
- data: {
129
- scopes,
130
- },
223
+ const response: {
224
+ service_terms?:
225
+ | {
226
+ tag: string;
227
+ agreed_at?: string | null;
228
+ agreed: boolean;
229
+ required: boolean;
230
+ revocable: boolean;
231
+ agreed_by?: string | null;
232
+ }[]
233
+ | null;
234
+ } = await Kakao.API.request({
235
+ url: '/v2/user/service_terms',
236
+ data: filterNonNullishKeys({ result, tags: tags?.join(',') }),
131
237
  });
238
+
239
+ return (response.service_terms ?? []).map(
240
+ (term): KakaoServiceTerms =>
241
+ filterNonNullishKeys({
242
+ tag: term.tag,
243
+ agreedAt: term.agreed_at ? isoToUnix(term.agreed_at) : undefined,
244
+ agreed: term.agreed,
245
+ required: term.required,
246
+ revocable: term.revocable,
247
+ agreedBy:
248
+ term.agreed_by == null
249
+ ? undefined
250
+ : term.agreed_by === 'KAUTH' || term.agreed_by === 'KAPI'
251
+ ? term.agreed_by
252
+ : 'unknown',
253
+ }),
254
+ );
132
255
  }),
133
- serviceTerms: () =>
256
+ revokeServiceTerms: (tags) =>
134
257
  kRunWebAPI(async () => {
135
258
  const response: {
136
- service_terms?: {
137
- tag: string;
138
- agreed_at?: string;
139
- agreed: boolean;
140
- required: boolean;
141
- revocable: boolean;
142
- }[];
143
- } = await Kakao.API.request({ url: '/v2/user/service_terms' });
144
-
145
- return (response.service_terms ?? []).map((term) => ({
146
- tag: term.tag,
147
- agreedAt: isoToUnix(term.agreed_at),
148
- agreed: term.agreed,
149
- required: term.required,
150
- revocable: term.revocable,
151
- }));
152
- }),
153
- shippingAddresses: () =>
259
+ revoked_service_terms?: { tag: string; agreed: boolean }[] | null;
260
+ } = await Kakao.API.request({
261
+ url: '/v2/user/revoke/service_terms',
262
+ data: { tags: tags.join(',') },
263
+ });
264
+
265
+ return response.revoked_service_terms ?? [];
266
+ }),
267
+ shippingAddresses: ({ addressId, fromUpdatedAt, pageSize } = {}) =>
154
268
  kRunWebAPI(async () => {
155
- const response = camelCaseObject(
156
- await Kakao.API.request({ url: '/v1/user/shipping_address' }),
157
- ) as {
269
+ validateShippingAddressOptions({ addressId, fromUpdatedAt, pageSize });
270
+
271
+ const query = new URLSearchParams(
272
+ Object.entries(
273
+ filterNonNullishKeys(
274
+ addressId != null
275
+ ? { address_id: addressId }
276
+ : { from_updated_at: fromUpdatedAt, page_size: pageSize },
277
+ ),
278
+ ).map(([key, value]) => [key, String(value)]),
279
+ );
280
+
281
+ const { body } = await kFetch<Record<string, unknown>>(
282
+ `https://kapi.kakao.com/v1/user/shipping_address?${query}`,
283
+ { headers: { Authorization: `Bearer ${Kakao.Auth.getAccessToken() ?? ''}` } },
284
+ );
285
+
286
+ const response = camelCaseObject(body) as {
158
287
  userId?: number;
159
288
  shippingAddressesNeedsAgreement?: boolean;
160
289
  shippingAddresses?: (Omit<
@@ -172,17 +301,52 @@ const KakaoUser: KakaoUserAPI = {
172
301
  ...address,
173
302
  updatedAt: isoToUnix(address.updatedAt),
174
303
  })),
175
- } as KakaoShippingAddressResult;
304
+ };
176
305
  }),
177
- me: () =>
306
+ selectShippingAddress: () =>
178
307
  kRunWebAPI(async () => {
179
- const ret = camelCaseObject(await Kakao.API.request({ url: '/v2/user/me' })) as {
308
+ try {
309
+ const { address_id } = await Kakao.Auth.selectShippingAddress();
310
+
311
+ return address_id;
312
+ } catch (error) {
313
+ if (error != null && typeof error === 'object' && 'error_code' in error) {
314
+ throw {
315
+ code: String(error.error_code),
316
+ message: 'error_msg' in error ? String(error.error_msg) : undefined,
317
+ };
318
+ }
319
+
320
+ throw error;
321
+ }
322
+ }),
323
+ updateProfile: ({ properties }) =>
324
+ kRunWebAPI(async () => {
325
+ await Kakao.API.request({ url: '/v1/user/update_profile', data: { properties } });
326
+ }),
327
+ me: ({ propertyKeys, secureResource = true } = {}) =>
328
+ kRunWebAPI(async () => {
329
+ const response: { properties?: Record<string, string> } = await Kakao.API.request({
330
+ url: '/v2/user/me',
331
+ data: filterNonNullishKeys({
332
+ property_keys: propertyKeys,
333
+ secure_resource: secureResource,
334
+ }),
335
+ });
336
+
337
+ const ret = camelCaseObject(response) as {
180
338
  id: number;
339
+ hasSignedUp?: boolean | null;
340
+ forPartner?: { uuid?: string };
341
+ groupUserToken?: string;
181
342
  connectedAt?: string;
182
343
  synchedAt?: string;
183
344
  kakaoAccount?: Partial<KakaoUserProfile> & {
184
345
  profile?: Partial<
185
- Pick<KakaoUserProfile, 'nickname' | 'profileImageUrl' | 'thumbnailImageUrl'>
346
+ Pick<
347
+ KakaoUserProfile,
348
+ 'nickname' | 'profileImageUrl' | 'thumbnailImageUrl' | 'isDefaultImage'
349
+ >
186
350
  >;
187
351
  };
188
352
  properties?: { nickname?: string; profileImage?: string; thumbnailImage?: string };
@@ -190,9 +354,22 @@ const KakaoUser: KakaoUserAPI = {
190
354
  const kakaoAccount = ret.kakaoAccount ?? {};
191
355
  const properties = ret.properties ?? {};
192
356
  const profile = kakaoAccount.profile ?? {};
357
+ const legalGender = kakaoAccount.legalGender;
193
358
 
194
359
  return {
195
360
  id: ret.id,
361
+ properties: response.properties ?? {},
362
+ hasSignedUp: ret.hasSignedUp ?? null,
363
+ uuid: ret.forPartner?.uuid ?? null,
364
+ groupUserToken: ret.groupUserToken ?? null,
365
+ isDefaultImage: profile.isDefaultImage ?? null,
366
+ isLeapMonth: kakaoAccount.isLeapMonth ?? null,
367
+ legalName: kakaoAccount.legalName ?? null,
368
+ legalNameNeedsAgreement: kakaoAccount.legalNameNeedsAgreement ?? null,
369
+ legalBirthDate: kakaoAccount.legalBirthDate ?? null,
370
+ legalBirthDateNeedsAgreement: kakaoAccount.legalBirthDateNeedsAgreement,
371
+ legalGender: legalGender === 'female' || legalGender === 'male' ? legalGender : null,
372
+ legalGenderNeedsAgreement: kakaoAccount.legalGenderNeedsAgreement ?? null,
196
373
  email: kakaoAccount.email,
197
374
  name: kakaoAccount.name,
198
375
  nickname: profile.nickname ?? properties.nickname,
@@ -218,23 +395,41 @@ const KakaoUser: KakaoUserAPI = {
218
395
  nameNeedsAgreement: kakaoAccount.nameNeedsAgreement,
219
396
  profileImageNeedsAgreement: kakaoAccount.profileImageNeedsAgreement,
220
397
  profileNicknameNeedsAgreement: kakaoAccount.profileNicknameNeedsAgreement,
221
- legalBirthDateNeedsAgreement: kakaoAccount.legalBirthDateNeedsAgreement,
222
398
  connectedAt: isoToUnix(ret.connectedAt),
223
399
  synchedAt: isoToUnix(ret.synchedAt),
224
400
  } as KakaoUserProfile;
225
401
  }),
226
402
  };
227
403
 
404
+ export function revokeScopesWithResult(scopes: string[]): Promise<KakaoScopeInfo[]> {
405
+ return kRunWebAPI(async () => {
406
+ const response = await Kakao.API.request({
407
+ url: '/v2/user/revoke/scopes',
408
+ data: { scopes },
409
+ });
410
+
411
+ return camelCaseObject(response.scopes ?? []) as KakaoScopeInfo[];
412
+ });
413
+ }
414
+
228
415
  export const {
416
+ loginWithKakao,
417
+ refreshToken,
418
+ issueAccessTokenWithCode,
229
419
  isKakaoTalkLoginAvailable,
230
420
  isLogined,
421
+ hasToken,
231
422
  login,
423
+ signup,
232
424
  logout,
233
425
  me,
426
+ updateProfile,
234
427
  revokeScopes,
235
428
  scopes,
236
429
  serviceTerms,
430
+ revokeServiceTerms,
237
431
  shippingAddresses,
432
+ selectShippingAddress,
238
433
  unlink,
239
434
  setAccessTokenWeb,
240
435
  issueAccessTokenWithCodeWeb,
@@ -1,9 +1,11 @@
1
1
  import type { TurboModule } from 'react-native';
2
2
  import { TurboModuleRegistry } from 'react-native';
3
+ import type { UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes';
3
4
 
4
5
  import type {
5
6
  KakaoAccessTokenInfo,
6
7
  KakaoLoginToken,
8
+ KakaoRevokedServiceTerms,
7
9
  KakaoScopeInfo,
8
10
  KakaoServiceTerms,
9
11
  KakaoShippingAddressResult,
@@ -16,17 +18,46 @@ export interface Spec extends TurboModule {
16
18
  serviceTerms: string[],
17
19
  prompts: string[],
18
20
  useKakaoAccountLogin: boolean,
21
+ iosLaunchMethod: string,
19
22
  scopes?: string[],
20
23
  nonce?: string,
24
+ loginHint?: string,
25
+ channelPublicIds?: string[],
26
+ androidRequestCode?: number,
21
27
  ): Promise<KakaoLoginToken>;
28
+ loginWithKakao(
29
+ uiMode: string,
30
+ orientation: string,
31
+ launchMethod: string,
32
+ prompts: string[],
33
+ loginHint?: string,
34
+ nonce?: string,
35
+ channelPublicIds?: string[],
36
+ serviceTerms?: string[],
37
+ ): Promise<KakaoLoginToken>;
38
+ issueAccessTokenWithCode(
39
+ code: string,
40
+ codeVerifier?: string,
41
+ redirectUri?: string,
42
+ ): Promise<KakaoLoginToken>;
43
+ refreshToken(token?: UnsafeObject, iosLoggingEnabled?: boolean): Promise<KakaoLoginToken>;
44
+ signup(properties?: UnsafeObject): Promise<void>;
22
45
  logout(): Promise<void>;
23
46
  unlink(): Promise<void>;
24
47
  isLogined(): Promise<boolean>;
48
+ hasToken(): Promise<boolean>;
25
49
  scopes(scopes?: string[]): Promise<KakaoScopeInfo[]>;
26
- revokeScopes(scopes: string[]): Promise<void>;
27
- serviceTerms(): Promise<KakaoServiceTerms[]>;
28
- shippingAddresses(): Promise<KakaoShippingAddressResult>;
29
- me(): Promise<KakaoUser>;
50
+ revokeScopes(scopes: string[]): Promise<KakaoScopeInfo[]>;
51
+ serviceTerms(result?: string, tags?: string[]): Promise<KakaoServiceTerms[]>;
52
+ revokeServiceTerms(tags: string[]): Promise<KakaoRevokedServiceTerms[]>;
53
+ shippingAddresses(
54
+ addressId?: number,
55
+ fromUpdatedAt?: number,
56
+ pageSize?: number,
57
+ ): Promise<KakaoShippingAddressResult>;
58
+ selectShippingAddress(): Promise<number>;
59
+ me(propertyKeys?: string[], secureResource?: boolean): Promise<KakaoUser>;
60
+ updateProfile(properties: UnsafeObject): Promise<void>;
30
61
  getAccessToken(): Promise<KakaoAccessTokenInfo>;
31
62
  }
32
63
 
@@ -0,0 +1,30 @@
1
+ import { kAssert } from '@react-native-kakao/core';
2
+
3
+ export function validateShippingAddressOptions({
4
+ addressId,
5
+ fromUpdatedAt,
6
+ pageSize,
7
+ }: {
8
+ addressId?: number;
9
+ fromUpdatedAt?: number;
10
+ pageSize?: number;
11
+ }) {
12
+ if (addressId != null) {
13
+ kAssert(
14
+ Number.isSafeInteger(addressId),
15
+ '[shippingAddresses] addressId must be a safe integer',
16
+ );
17
+
18
+ return;
19
+ }
20
+
21
+ kAssert(
22
+ fromUpdatedAt == null || (Number.isSafeInteger(fromUpdatedAt) && fromUpdatedAt >= 0),
23
+ '[shippingAddresses] fromUpdatedAt must be a non-negative safe integer in Unix seconds',
24
+ );
25
+
26
+ kAssert(
27
+ pageSize == null || (Number.isInteger(pageSize) && pageSize >= 2 && pageSize <= 2147483647),
28
+ '[shippingAddresses] pageSize must be an integer between 2 and 2147483647',
29
+ );
30
+ }