@react-native-kakao/user 3.0.0 → 3.1.0-next.1

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,21 +1,30 @@
1
1
  import { camelCaseObject, filterNonNullishKeys, is } from '@mj-studio/js-util';
2
- import { kFetch, kFetchFormUrlEncoded, kGlobalStorage, kRunWebAPI } from '@react-native-kakao/core';
3
- import * as querystring from 'querystring';
2
+ import {
3
+ kAssert,
4
+ kCreateWebError,
5
+ kFetch,
6
+ kFetchFormUrlEncoded,
7
+ kGlobalStorage,
8
+ kRunWebAPI,
9
+ } from '@react-native-kakao/core';
4
10
 
5
11
  import {
6
12
  type KakaoLoginToken,
7
13
  type KakaoScopeInfo,
8
- type KakaoServiceTerms,
9
14
  type KakaoShippingAddressResult,
10
- type KakaoUser,
15
+ type KakaoUser as KakaoUserProfile,
11
16
  type KakaoUserAPI,
12
17
  } from './index';
18
+ import { validateShippingAddressOptions } from './validateShippingAddressOptions';
13
19
 
14
20
  declare const Kakao: {
21
+ isInitialized: () => boolean;
15
22
  Auth: {
16
23
  authorize: Function;
17
24
  logout: Function;
18
25
  setAccessToken: Function;
26
+ selectShippingAddress: () => Promise<{ address_id: number }>;
27
+ getAccessToken: () => string | null;
19
28
  };
20
29
  API: { request: Function };
21
30
  };
@@ -29,19 +38,26 @@ function isoToUnix(value?: string | number) {
29
38
  }
30
39
 
31
40
  const KakaoUser: KakaoUserAPI = {
41
+ loginWithKakao: () =>
42
+ kRunWebAPI(() => {
43
+ throw {
44
+ code: 'Package-UnsupportedPlatform',
45
+ message:
46
+ 'loginWithKakao is supported on Android and iOS. Use login with web.redirectUri on web.',
47
+ };
48
+ }),
49
+ refreshToken: () =>
50
+ kRunWebAPI(() => {
51
+ throw {
52
+ code: 'Package-UnsupportedPlatform',
53
+ message: 'The Kakao JavaScript SDK does not support refreshToken.',
54
+ };
55
+ }),
56
+ hasToken: () => kRunWebAPI(() => Kakao.Auth.getAccessToken() != null),
32
57
  getAccessToken: () =>
33
58
  kRunWebAPI(async () => {
34
- const { id, expires_in, app_id } = (
35
- await kFetch<{
36
- id: number;
37
- expires_in: number;
38
- app_id: number;
39
- }>('https://kapi.kakao.com/v1/user/access_token_info', {
40
- headers: {
41
- ...kGlobalStorage.headers,
42
- },
43
- })
44
- ).body;
59
+ const { id, expires_in, app_id }: { id: number; expires_in: number; app_id: number } =
60
+ await Kakao.API.request({ url: '/v1/user/access_token_info' });
45
61
 
46
62
  return {
47
63
  id,
@@ -49,7 +65,62 @@ const KakaoUser: KakaoUserAPI = {
49
65
  appId: app_id,
50
66
  };
51
67
  }),
52
- issueAccessTokenWithCodeWeb: ({ clientSecret, code, redirectUri }) =>
68
+ issueAccessTokenWithCode: ({ code, codeVerifier, redirectUri, web }) =>
69
+ kRunWebAPI(async () => {
70
+ kAssert(
71
+ is.string(code) && code.length > 0 && is.string(redirectUri) && redirectUri.length > 0,
72
+ '[issueAccessTokenWithCode] code and redirectUri are required on web',
73
+ );
74
+
75
+ if (typeof Kakao === 'undefined' || !Kakao.isInitialized()) {
76
+ kCreateWebError({
77
+ code: 'Package-SDKNotInitialized',
78
+ message: 'Initialize the Kakao SDK before exchanging an authorization code.',
79
+ });
80
+ }
81
+
82
+ const token = await KakaoUser.issueAccessTokenWithCodeWeb({
83
+ code,
84
+ codeVerifier,
85
+ redirectUri: redirectUri!,
86
+ clientSecret: web?.clientSecret,
87
+ });
88
+
89
+ if (
90
+ !is.string(token.accessToken) ||
91
+ !token.accessToken ||
92
+ !is.string(token.refreshToken) ||
93
+ !token.refreshToken ||
94
+ !is.string(token.tokenType) ||
95
+ (token.idToken != null && !is.string(token.idToken)) ||
96
+ !Number.isFinite(token.expiresIn) ||
97
+ token.expiresIn < 0 ||
98
+ !Number.isFinite(token.refreshTokenExpiresIn) ||
99
+ token.refreshTokenExpiresIn < 0 ||
100
+ (token.scope != null && !is.string(token.scope))
101
+ ) {
102
+ kCreateWebError({
103
+ code: 'Package-APIResponseNotFound',
104
+ message: 'issueAccessTokenWithCode returned invalid token data.',
105
+ });
106
+ }
107
+
108
+ const issuedAt = Math.floor(Date.now() / 1000);
109
+ KakaoUser.setAccessTokenWeb(token.accessToken);
110
+
111
+ return {
112
+ accessToken: token.accessToken,
113
+ refreshToken: token.refreshToken,
114
+ tokenType: token.tokenType,
115
+ idToken: token.idToken,
116
+ accessTokenExpiresIn: token.expiresIn,
117
+ refreshTokenExpiresIn: token.refreshTokenExpiresIn,
118
+ accessTokenExpiresAt: issuedAt + token.expiresIn,
119
+ refreshTokenExpiresAt: issuedAt + token.refreshTokenExpiresIn,
120
+ scopes: token.scope?.split(' ').filter(Boolean) ?? [],
121
+ };
122
+ }),
123
+ issueAccessTokenWithCodeWeb: ({ clientSecret, code, codeVerifier, redirectUri }) =>
53
124
  kRunWebAPI(async () => {
54
125
  const res = await kFetchFormUrlEncoded<{
55
126
  token_type: string;
@@ -64,8 +135,9 @@ const KakaoUser: KakaoUserAPI = {
64
135
  body: filterNonNullishKeys({
65
136
  grant_type: 'authorization_code',
66
137
  client_id: kGlobalStorage.restApiKey,
67
- redirect_uri: encodeURIComponent(redirectUri),
138
+ redirect_uri: redirectUri,
68
139
  code,
140
+ code_verifier: codeVerifier,
69
141
  client_secret: clientSecret,
70
142
  }),
71
143
  }).then((r) => r.body);
@@ -73,32 +145,49 @@ const KakaoUser: KakaoUserAPI = {
73
145
  return camelCaseObject(res) as any;
74
146
  }),
75
147
  setAccessTokenWeb: (token: string) => {
148
+ Kakao.Auth.setAccessToken(token);
76
149
  kGlobalStorage.accessToken = token;
77
-
78
- return new Promise((r) => {
79
- Kakao.Auth.setAccessToken(token);
80
- r(42);
81
- });
82
150
  },
83
151
  login: (params): Promise<KakaoLoginToken> =>
84
152
  kRunWebAPI(() => {
85
153
  const { loginHint, nonce, prompt, redirectUri, scope, state, throughTalk, serviceTerms } =
86
154
  params?.web ?? {};
155
+ const channelPublicIds = params?.channelPublicIds;
87
156
 
88
157
  return Kakao.Auth.authorize(
89
158
  filterNonNullishKeys({
90
- loginHint,
159
+ loginHint: params?.loginHint ?? loginHint,
160
+ channelPublicId: channelPublicIds?.length ? channelPublicIds.join(',') : undefined,
91
161
  nonce: params?.nonce ?? nonce,
92
- prompt: prompt?.join(','),
162
+ prompt: (
163
+ prompt ??
164
+ params?.prompts?.map((value) =>
165
+ value === 'SelectAccount' ? 'select_account' : value.toLowerCase(),
166
+ )
167
+ )?.join(','),
93
168
  redirectUri,
94
- scope: scope?.join(','),
95
- serviceTerms: serviceTerms?.join(','),
169
+ scope: (scope ?? params?.scopes)?.join(','),
170
+ serviceTerms: (serviceTerms ?? params?.serviceTerms)?.join(','),
96
171
  state,
97
- throughTalk,
172
+ throughTalk: throughTalk ?? (params?.useKakaoAccountLogin ? false : undefined),
98
173
  }),
99
174
  );
100
175
  }),
101
- logout: () => kRunWebAPI(() => Kakao.Auth.logout()),
176
+ signup: ({ properties } = {}) =>
177
+ kRunWebAPI(async () => {
178
+ await Kakao.API.request({
179
+ url: '/v1/user/signup',
180
+ data: filterNonNullishKeys({ properties }),
181
+ });
182
+ }),
183
+ logout: () =>
184
+ kRunWebAPI(async () => {
185
+ try {
186
+ await Kakao.Auth.logout();
187
+ } finally {
188
+ kGlobalStorage.clearAccessToken();
189
+ }
190
+ }),
102
191
  isLogined: async (): Promise<boolean> => {
103
192
  try {
104
193
  const { appId } = await KakaoUser.getAccessToken();
@@ -109,81 +198,214 @@ const KakaoUser: KakaoUserAPI = {
109
198
  }
110
199
  },
111
200
  isKakaoTalkLoginAvailable: async () => false,
112
- unlink: () => kRunWebAPI(() => Kakao.API.request({ url: '/v1/user/unlink' })),
201
+ unlink: () =>
202
+ kRunWebAPI(async () => {
203
+ await Kakao.API.request({ url: '/v1/user/unlink' });
204
+ Kakao.Auth.setAccessToken(null);
205
+ kGlobalStorage.clearAccessToken();
206
+ }),
113
207
  scopes: (scopes?: string[]) =>
114
208
  kRunWebAPI(async () => {
115
209
  const response = await Kakao.API.request({
116
- url: `/v2/user/scopes${scopes ? '?' + querystring.stringify({ scopes }) : ''}`,
210
+ url: '/v2/user/scopes',
211
+ data: scopes?.length ? { scopes } : undefined,
117
212
  });
118
213
 
119
214
  return camelCaseObject(response.scopes) as KakaoScopeInfo[];
120
215
  }),
121
216
  revokeScopes: (scopes: string[]) =>
122
- kRunWebAPI(() =>
123
- Kakao.API.request({
217
+ kRunWebAPI(async () => {
218
+ await Kakao.API.request({
124
219
  url: '/v2/user/revoke/scopes',
125
220
  data: {
126
221
  scopes,
127
222
  },
128
- }),
129
- ),
130
- serviceTerms: () =>
131
- kRunWebAPI(
132
- async () =>
133
- camelCaseObject(
134
- (
135
- await Kakao.API.request({
136
- url: '/v2/user/service_terms',
137
- })
138
- ).service_terms.map((v) => ({
139
- ...v,
140
- agreed_at: v.agreed_at ? isoToUnix(v.agreed_at) : null,
141
- })),
142
- ) as KakaoServiceTerms[],
143
- ),
144
- shippingAddresses: () =>
145
- kRunWebAPI(
146
- async () =>
147
- camelCaseObject(
148
- await Kakao.API.request({
149
- url: '/v1/user/shipping_address',
150
- }),
151
- ) as KakaoShippingAddressResult,
152
- ),
153
- me: () =>
223
+ });
224
+ }),
225
+ serviceTerms: ({ result, tags } = {}) =>
226
+ kRunWebAPI(async () => {
227
+ const response: {
228
+ service_terms?:
229
+ | {
230
+ tag: string;
231
+ agreed_at?: string | null;
232
+ agreed: boolean;
233
+ required: boolean;
234
+ revocable: boolean;
235
+ }[]
236
+ | null;
237
+ } = await Kakao.API.request({
238
+ url: '/v2/user/service_terms',
239
+ data: filterNonNullishKeys({ result, tags: tags?.join(',') }),
240
+ });
241
+
242
+ return (response.service_terms ?? []).map((term) => ({
243
+ tag: term.tag,
244
+ agreedAt: term.agreed_at ? isoToUnix(term.agreed_at) : undefined,
245
+ agreed: term.agreed,
246
+ required: term.required,
247
+ revocable: term.revocable,
248
+ }));
249
+ }),
250
+ revokeServiceTerms: (tags) =>
251
+ kRunWebAPI(async () => {
252
+ const response: {
253
+ revoked_service_terms?: { tag: string; agreed: boolean }[] | null;
254
+ } = await Kakao.API.request({
255
+ url: '/v2/user/revoke/service_terms',
256
+ data: { tags: tags.join(',') },
257
+ });
258
+
259
+ return response.revoked_service_terms ?? [];
260
+ }),
261
+ shippingAddresses: ({ addressId, fromUpdatedAt, pageSize } = {}) =>
154
262
  kRunWebAPI(async () => {
155
- const ret: any = camelCaseObject(await Kakao.API.request({ url: '/v2/user/me' }));
263
+ validateShippingAddressOptions({ addressId, fromUpdatedAt, pageSize });
264
+
265
+ const query = new URLSearchParams(
266
+ Object.entries(
267
+ filterNonNullishKeys(
268
+ addressId != null
269
+ ? { address_id: addressId }
270
+ : { from_updated_at: fromUpdatedAt, page_size: pageSize },
271
+ ),
272
+ ).map(([key, value]) => [key, String(value)]),
273
+ );
274
+
275
+ const { body } = await kFetch<Record<string, unknown>>(
276
+ `https://kapi.kakao.com/v1/user/shipping_address?${query}`,
277
+ { headers: { Authorization: `Bearer ${Kakao.Auth.getAccessToken() ?? ''}` } },
278
+ );
279
+
280
+ const response = camelCaseObject(body) as {
281
+ userId?: number;
282
+ shippingAddressesNeedsAgreement?: boolean;
283
+ shippingAddresses?: (Omit<
284
+ KakaoShippingAddressResult['shippingAddresses'][number],
285
+ 'updatedAt'
286
+ > & {
287
+ updatedAt?: string | number;
288
+ })[];
289
+ };
290
+
291
+ return {
292
+ userId: response.userId,
293
+ needsAgreement: response.shippingAddressesNeedsAgreement,
294
+ shippingAddresses: (response.shippingAddresses ?? []).map((address) => ({
295
+ ...address,
296
+ updatedAt: isoToUnix(address.updatedAt),
297
+ })),
298
+ };
299
+ }),
300
+ selectShippingAddress: () =>
301
+ kRunWebAPI(async () => {
302
+ try {
303
+ const { address_id } = await Kakao.Auth.selectShippingAddress();
304
+
305
+ return address_id;
306
+ } catch (error) {
307
+ if (error != null && typeof error === 'object' && 'error_code' in error) {
308
+ throw {
309
+ code: String(error.error_code),
310
+ message: 'error_msg' in error ? String(error.error_msg) : undefined,
311
+ };
312
+ }
313
+
314
+ throw error;
315
+ }
316
+ }),
317
+ updateProfile: ({ properties }) =>
318
+ kRunWebAPI(async () => {
319
+ await Kakao.API.request({ url: '/v1/user/update_profile', data: { properties } });
320
+ }),
321
+ me: ({ secureResource = true } = {}) =>
322
+ kRunWebAPI(async () => {
323
+ const ret = camelCaseObject(
324
+ await Kakao.API.request({ url: '/v2/user/me', data: { secure_resource: secureResource } }),
325
+ ) as {
326
+ id: number;
327
+ hasSignedUp?: boolean | null;
328
+ forPartner?: { uuid?: string };
329
+ groupUserToken?: string;
330
+ connectedAt?: string;
331
+ synchedAt?: string;
332
+ kakaoAccount?: Partial<KakaoUserProfile> & {
333
+ profile?: Partial<
334
+ Pick<
335
+ KakaoUserProfile,
336
+ 'nickname' | 'profileImageUrl' | 'thumbnailImageUrl' | 'isDefaultImage'
337
+ >
338
+ >;
339
+ };
340
+ properties?: { nickname?: string; profileImage?: string; thumbnailImage?: string };
341
+ };
156
342
  const kakaoAccount = ret.kakaoAccount ?? {};
157
343
  const properties = ret.properties ?? {};
344
+ const profile = kakaoAccount.profile ?? {};
345
+ const legalGender = kakaoAccount.legalGender;
158
346
 
159
347
  return {
160
- ...ret,
161
- ...properties,
162
- ...kakaoAccount,
348
+ id: ret.id,
349
+ hasSignedUp: ret.hasSignedUp ?? null,
350
+ uuid: ret.forPartner?.uuid ?? null,
351
+ groupUserToken: ret.groupUserToken ?? null,
352
+ isDefaultImage: profile.isDefaultImage ?? null,
353
+ isLeapMonth: kakaoAccount.isLeapMonth ?? null,
354
+ legalName: kakaoAccount.legalName ?? null,
355
+ legalNameNeedsAgreement: kakaoAccount.legalNameNeedsAgreement ?? null,
356
+ legalBirthDate: kakaoAccount.legalBirthDate ?? null,
357
+ legalBirthDateNeedsAgreement: kakaoAccount.legalBirthDateNeedsAgreement,
358
+ legalGender: legalGender === 'female' || legalGender === 'male' ? legalGender : null,
359
+ legalGenderNeedsAgreement: kakaoAccount.legalGenderNeedsAgreement ?? null,
163
360
  email: kakaoAccount.email,
164
- name: null,
165
- nickname: kakaoAccount.profile.nickname,
166
- profileImageUrl: properties.profileImage,
167
- thumbnailImageUrl: properties.thumbnailImage,
168
- phoneNumber: kakaoAccount.profile.phoneNumber,
169
- isEmailValid: ret.isEmailValid,
170
- isEmailVerified: ret.isEmailVerified,
361
+ name: kakaoAccount.name,
362
+ nickname: profile.nickname ?? properties.nickname,
363
+ profileImageUrl: profile.profileImageUrl ?? properties.profileImage,
364
+ thumbnailImageUrl: profile.thumbnailImageUrl ?? properties.thumbnailImage,
365
+ phoneNumber: kakaoAccount.phoneNumber,
366
+ isEmailValid: kakaoAccount.isEmailValid,
367
+ isEmailVerified: kakaoAccount.isEmailVerified,
368
+ ageRange: kakaoAccount.ageRange,
369
+ birthday: kakaoAccount.birthday,
370
+ birthdayType: kakaoAccount.birthdayType,
371
+ birthyear: kakaoAccount.birthyear,
372
+ gender: kakaoAccount.gender,
373
+ isKorean: kakaoAccount.isKorean,
374
+ ageRangeNeedsAgreement: kakaoAccount.ageRangeNeedsAgreement,
375
+ birthdayNeedsAgreement: kakaoAccount.birthdayNeedsAgreement,
376
+ birthyearNeedsAgreement: kakaoAccount.birthyearNeedsAgreement,
377
+ emailNeedsAgreement: kakaoAccount.emailNeedsAgreement,
378
+ genderNeedsAgreement: kakaoAccount.genderNeedsAgreement,
379
+ isKoreanNeedsAgreement: kakaoAccount.isKoreanNeedsAgreement,
380
+ phoneNumberNeedsAgreement: kakaoAccount.phoneNumberNeedsAgreement,
381
+ profileNeedsAgreement: kakaoAccount.profileNeedsAgreement,
382
+ nameNeedsAgreement: kakaoAccount.nameNeedsAgreement,
383
+ profileImageNeedsAgreement: kakaoAccount.profileImageNeedsAgreement,
384
+ profileNicknameNeedsAgreement: kakaoAccount.profileNicknameNeedsAgreement,
171
385
  connectedAt: isoToUnix(ret.connectedAt),
172
386
  synchedAt: isoToUnix(ret.synchedAt),
173
- } as KakaoUser;
387
+ } as KakaoUserProfile;
174
388
  }),
175
389
  };
176
390
 
177
391
  export const {
392
+ loginWithKakao,
393
+ refreshToken,
394
+ issueAccessTokenWithCode,
178
395
  isKakaoTalkLoginAvailable,
179
396
  isLogined,
397
+ hasToken,
180
398
  login,
399
+ signup,
181
400
  logout,
182
401
  me,
402
+ updateProfile,
183
403
  revokeScopes,
184
404
  scopes,
185
405
  serviceTerms,
406
+ revokeServiceTerms,
186
407
  shippingAddresses,
408
+ selectShippingAddress,
187
409
  unlink,
188
410
  setAccessTokenWeb,
189
411
  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,
@@ -18,15 +20,43 @@ export interface Spec extends TurboModule {
18
20
  useKakaoAccountLogin: boolean,
19
21
  scopes?: string[],
20
22
  nonce?: string,
23
+ loginHint?: string,
24
+ channelPublicIds?: string[],
25
+ androidRequestCode?: number,
21
26
  ): Promise<KakaoLoginToken>;
27
+ loginWithKakao(
28
+ uiMode: string,
29
+ orientation: string,
30
+ launchMethod: string,
31
+ prompts: string[],
32
+ loginHint?: string,
33
+ nonce?: string,
34
+ channelPublicIds?: string[],
35
+ serviceTerms?: string[],
36
+ ): Promise<KakaoLoginToken>;
37
+ issueAccessTokenWithCode(
38
+ code: string,
39
+ codeVerifier?: string,
40
+ redirectUri?: string,
41
+ ): Promise<KakaoLoginToken>;
42
+ refreshToken(token?: UnsafeObject, iosLoggingEnabled?: boolean): Promise<KakaoLoginToken>;
43
+ signup(properties?: UnsafeObject): Promise<void>;
22
44
  logout(): Promise<void>;
23
45
  unlink(): Promise<void>;
24
46
  isLogined(): Promise<boolean>;
47
+ hasToken(): Promise<boolean>;
25
48
  scopes(scopes?: string[]): Promise<KakaoScopeInfo[]>;
26
49
  revokeScopes(scopes: string[]): Promise<void>;
27
- serviceTerms(): Promise<KakaoServiceTerms[]>;
28
- shippingAddresses(): Promise<KakaoShippingAddressResult>;
29
- me(): Promise<KakaoUser>;
50
+ serviceTerms(result?: string, tags?: string[]): Promise<KakaoServiceTerms[]>;
51
+ revokeServiceTerms(tags: string[]): Promise<KakaoRevokedServiceTerms[]>;
52
+ shippingAddresses(
53
+ addressId?: number,
54
+ fromUpdatedAt?: number,
55
+ pageSize?: number,
56
+ ): Promise<KakaoShippingAddressResult>;
57
+ selectShippingAddress(): Promise<number>;
58
+ me(secureResource?: boolean): Promise<KakaoUser>;
59
+ updateProfile(properties: UnsafeObject): Promise<void>;
30
60
  getAccessToken(): Promise<KakaoAccessTokenInfo>;
31
61
  }
32
62
 
@@ -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
+ }