@planningcenter/chat-react-native 2.2.2-rc.0 → 2.3.0-rc.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.
Files changed (111) hide show
  1. package/build/components/conversations.d.ts.map +1 -1
  2. package/build/components/conversations.js +4 -0
  3. package/build/components/conversations.js.map +1 -1
  4. package/build/components/display/action_button.d.ts +8 -0
  5. package/build/components/display/action_button.d.ts.map +1 -0
  6. package/build/components/display/action_button.js +44 -0
  7. package/build/components/display/action_button.js.map +1 -0
  8. package/build/contexts/api_provider.js +2 -2
  9. package/build/contexts/api_provider.js.map +1 -1
  10. package/build/contexts/chat_context.d.ts +2 -1
  11. package/build/contexts/chat_context.d.ts.map +1 -1
  12. package/build/contexts/chat_context.js +3 -1
  13. package/build/contexts/chat_context.js.map +1 -1
  14. package/build/hooks/index.d.ts +2 -0
  15. package/build/hooks/index.d.ts.map +1 -1
  16. package/build/hooks/index.js +2 -0
  17. package/build/hooks/index.js.map +1 -1
  18. package/build/hooks/use_api.d.ts +383 -0
  19. package/build/hooks/use_api.d.ts.map +1 -0
  20. package/build/hooks/use_api.js +40 -0
  21. package/build/hooks/use_api.js.map +1 -0
  22. package/build/hooks/use_api_client.d.ts +5 -3
  23. package/build/hooks/use_api_client.d.ts.map +1 -1
  24. package/build/hooks/use_api_client.js +1 -1
  25. package/build/hooks/use_api_client.js.map +1 -1
  26. package/build/hooks/use_chat_permissions.d.ts +172 -0
  27. package/build/hooks/use_chat_permissions.d.ts.map +1 -0
  28. package/build/hooks/use_chat_permissions.js +17 -0
  29. package/build/hooks/use_chat_permissions.js.map +1 -0
  30. package/build/hooks/use_conversation.d.ts.map +1 -1
  31. package/build/hooks/use_conversation.js +6 -5
  32. package/build/hooks/use_conversation.js.map +1 -1
  33. package/build/hooks/use_suspense_api.d.ts +13 -4
  34. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  35. package/build/hooks/use_suspense_api.js +1 -0
  36. package/build/hooks/use_suspense_api.js.map +1 -1
  37. package/build/index.d.ts +1 -1
  38. package/build/index.d.ts.map +1 -1
  39. package/build/index.js +1 -1
  40. package/build/index.js.map +1 -1
  41. package/build/navigation/index.d.ts +123 -2
  42. package/build/navigation/index.d.ts.map +1 -1
  43. package/build/navigation/index.js +40 -4
  44. package/build/navigation/index.js.map +1 -1
  45. package/build/navigation/screenLayout.js +1 -1
  46. package/build/navigation/screenLayout.js.map +1 -1
  47. package/build/screens/create/conversation_create_screen.d.ts +9 -0
  48. package/build/screens/create/conversation_create_screen.d.ts.map +1 -0
  49. package/build/screens/create/conversation_create_screen.js +123 -0
  50. package/build/screens/create/conversation_create_screen.js.map +1 -0
  51. package/build/screens/create/conversation_filter_recipients_screen.d.ts +8 -0
  52. package/build/screens/create/conversation_filter_recipients_screen.d.ts.map +1 -0
  53. package/build/screens/create/conversation_filter_recipients_screen.js +52 -0
  54. package/build/screens/create/conversation_filter_recipients_screen.js.map +1 -0
  55. package/build/screens/create/conversation_select_recipients_screen.d.ts +8 -0
  56. package/build/screens/create/conversation_select_recipients_screen.d.ts.map +1 -0
  57. package/build/screens/create/conversation_select_recipients_screen.js +105 -0
  58. package/build/screens/create/conversation_select_recipients_screen.js.map +1 -0
  59. package/build/types/api_primitives.d.ts +15 -0
  60. package/build/types/api_primitives.d.ts.map +1 -1
  61. package/build/types/api_primitives.js.map +1 -1
  62. package/build/types/resources/app_grant.d.ts +6 -0
  63. package/build/types/resources/app_grant.d.ts.map +1 -0
  64. package/build/types/resources/app_grant.js +2 -0
  65. package/build/types/resources/app_grant.js.map +1 -0
  66. package/build/types/resources/groups/groups_group_resource.d.ts +12 -0
  67. package/build/types/resources/groups/groups_group_resource.d.ts.map +1 -0
  68. package/build/types/resources/groups/groups_group_resource.js +2 -0
  69. package/build/types/resources/groups/groups_group_resource.js.map +1 -0
  70. package/build/types/resources/groups/index.d.ts +2 -0
  71. package/build/types/resources/groups/index.d.ts.map +1 -0
  72. package/build/types/resources/groups/index.js +2 -0
  73. package/build/types/resources/groups/index.js.map +1 -0
  74. package/build/types/resources/index.d.ts +2 -0
  75. package/build/types/resources/index.d.ts.map +1 -1
  76. package/build/types/resources/index.js +2 -0
  77. package/build/types/resources/index.js.map +1 -1
  78. package/build/utils/client/client.d.ts +8 -5
  79. package/build/utils/client/client.d.ts.map +1 -1
  80. package/build/utils/client/client.js +22 -7
  81. package/build/utils/client/client.js.map +1 -1
  82. package/package.json +2 -2
  83. package/src/__tests__/client.ts +59 -4
  84. package/src/__tests__/hooks/useTheme.tsx +1 -1
  85. package/src/components/conversations.tsx +8 -0
  86. package/src/components/display/action_button.tsx +62 -0
  87. package/src/contexts/api_provider.tsx +2 -2
  88. package/src/contexts/chat_context.tsx +5 -2
  89. package/src/hooks/index.ts +2 -0
  90. package/src/hooks/use_api.ts +80 -0
  91. package/src/hooks/use_api_client.ts +13 -15
  92. package/src/hooks/use_chat_permissions.ts +20 -0
  93. package/src/hooks/use_conversation.ts +6 -5
  94. package/src/hooks/use_suspense_api.ts +16 -4
  95. package/src/index.tsx +1 -1
  96. package/src/navigation/index.tsx +46 -7
  97. package/src/navigation/screenLayout.tsx +1 -1
  98. package/src/screens/create/conversation_create_screen.tsx +148 -0
  99. package/src/screens/create/conversation_filter_recipients_screen.tsx +79 -0
  100. package/src/screens/create/conversation_select_recipients_screen.tsx +136 -0
  101. package/src/types/api_primitives.ts +12 -0
  102. package/src/types/resources/app_grant.ts +6 -0
  103. package/src/types/resources/groups/groups_group_resource.ts +12 -0
  104. package/src/types/resources/groups/index.ts +1 -0
  105. package/src/types/resources/index.ts +2 -0
  106. package/src/utils/client/client.ts +34 -11
  107. package/build/contexts/index.d.ts +0 -3
  108. package/build/contexts/index.d.ts.map +0 -1
  109. package/build/contexts/index.js +0 -3
  110. package/build/contexts/index.js.map +0 -1
  111. package/src/contexts/index.ts +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA"}
@@ -3,4 +3,6 @@ export * from './member';
3
3
  export * from './message';
4
4
  export * from './oauth_token';
5
5
  export * from './person';
6
+ export * from './groups';
7
+ export * from './app_grant';
6
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA","sourcesContent":["export * from './conversation'\nexport * from './member'\nexport * from './message'\nexport * from './oauth_token'\nexport * from './person'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA","sourcesContent":["export * from './conversation'\nexport * from './member'\nexport * from './message'\nexport * from './oauth_token'\nexport * from './person'\nexport * from './groups'\nexport * from './app_grant'\n"]}
@@ -1,11 +1,12 @@
1
- import { ApiCollection, ApiResource } from '../../types';
1
+ import { ApiCollection, ApiError, ApiResource } from '../../types';
2
2
  import { Session } from '../session';
3
3
  import { Uri } from '../uri';
4
4
  import { DeleteRequest, GetRequest, PatchRequest, PostRequest } from './types';
5
5
  type ClientArgs = {
6
6
  version: string;
7
7
  defaultHeaders?: Record<string, string>;
8
- onTokenExpired: () => void;
8
+ onForceLogout?: () => void;
9
+ onTokenExpired?: () => void;
9
10
  session: Session;
10
11
  app: string;
11
12
  };
@@ -13,13 +14,15 @@ export declare class Client {
13
14
  version: string;
14
15
  defaultHeaders: Record<string, string>;
15
16
  uri: Uri;
16
- onTokenExpired: () => void;
17
- constructor({ version, defaultHeaders, session, app, onTokenExpired }: ClientArgs);
17
+ onTokenExpired?: () => void;
18
+ onForceLogout?: () => void;
19
+ constructor({ version, defaultHeaders, session, app, onTokenExpired, onForceLogout, }: ClientArgs);
18
20
  get<T extends ApiCollection | ApiResource>(args: GetRequest): Promise<T>;
19
21
  patch(args: PatchRequest): Promise<any>;
20
22
  post(args: PostRequest): Promise<any>;
21
23
  delete(args: DeleteRequest): Promise<any>;
22
- handleTokenExpired: (response: Response) => Promise<never>;
24
+ handleNotOk: (response: Response) => Promise<never>;
25
+ parseErrorResponse: (response: Response) => Promise<ApiError | undefined>;
23
26
  get headers(): {
24
27
  'User-Agent': string;
25
28
  Authorization: string;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/utils/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAA;AAU5B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAe,MAAM,SAAS,CAAA;AAE3F,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,qBAAa,MAAM;IACjB,OAAO,EAAE,MAAM,CAAK;IACpB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAK;IAC3C,GAAG,EAAE,GAAG,CAAA;IACR,cAAc,EAAE,MAAM,IAAI,CAAA;gBAEd,EAAE,OAAO,EAAE,cAAmB,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,UAAU;IAOhF,GAAG,CAAC,CAAC,SAAS,aAAa,GAAG,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC;IAuCxE,KAAK,CAAC,IAAI,EAAE,YAAY;IASxB,IAAI,CAAC,IAAI,EAAE,WAAW;IAStB,MAAM,CAAC,IAAI,EAAE,aAAa;IAShC,kBAAkB,aAAc,QAAQ,oBAMvC;IAED,IAAI,OAAO;;;;;;MAQV;CACF;AAED,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/utils/client/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAA;AAU5B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAe,MAAM,SAAS,CAAA;AAE3F,KAAK,UAAU,GAAG;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,qBAAa,MAAM;IACjB,OAAO,EAAE,MAAM,CAAK;IACpB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAK;IAC3C,GAAG,EAAE,GAAG,CAAA;IACR,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;gBAEd,EACV,OAAO,EACP,cAAmB,EACnB,OAAO,EACP,GAAG,EACH,cAAc,EACd,aAAa,GACd,EAAE,UAAU;IAQP,GAAG,CAAC,CAAC,SAAS,aAAa,GAAG,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC;IAuCxE,KAAK,CAAC,IAAI,EAAE,YAAY;IASxB,IAAI,CAAC,IAAI,EAAE,WAAW;IAStB,MAAM,CAAC,IAAI,EAAE,aAAa;IAShC,WAAW,aAAoB,QAAQ,oBAWtC;IAED,kBAAkB,aAAoB,QAAQ,KAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAM7E;IAED,IAAI,OAAO;;;;;;MAQV;CACF;AAED,eAAe,MAAM,CAAA"}
@@ -5,11 +5,13 @@ export class Client {
5
5
  defaultHeaders = {};
6
6
  uri;
7
7
  onTokenExpired;
8
- constructor({ version, defaultHeaders = {}, session, app, onTokenExpired }) {
8
+ onForceLogout;
9
+ constructor({ version, defaultHeaders = {}, session, app, onTokenExpired, onForceLogout, }) {
9
10
  this.version = version;
10
11
  this.uri = new Uri({ session, app });
11
12
  this.defaultHeaders = defaultHeaders;
12
13
  this.onTokenExpired = onTokenExpired;
14
+ this.onForceLogout = onForceLogout;
13
15
  }
14
16
  async get(args) {
15
17
  const { walk, ...data } = args.data;
@@ -37,32 +39,45 @@ export class Client {
37
39
  });
38
40
  };
39
41
  const handler = isWalking ? walkRequest : makeRequest;
40
- return throwErrorIfFieldsMissing(handler, requestArgs).catch(this.handleTokenExpired);
42
+ return throwErrorIfFieldsMissing(handler, requestArgs).catch(this.handleNotOk);
41
43
  }
42
44
  async patch(args) {
43
45
  const headers = { ...this.headers, ...args.headers };
44
46
  const url = this.uri.appUrl(args.url);
45
47
  const requestArgs = { data: args.data, url, action: 'PATCH', headers };
46
- return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleTokenExpired);
48
+ return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleNotOk);
47
49
  }
48
50
  async post(args) {
49
51
  const headers = { ...this.headers, ...args.headers };
50
52
  const url = this.uri.appUrl(args.url);
51
53
  const requestArgs = { ...args, data: args.data, url, action: 'POST', headers };
52
- return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleTokenExpired);
54
+ return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleNotOk);
53
55
  }
54
56
  async delete(args) {
55
57
  const headers = { ...this.headers, ...args.headers };
56
58
  const url = this.uri.appUrl(args.url);
57
59
  const requestArgs = { url, action: 'DELETE', headers };
58
- return makeRequest(requestArgs).catch(this.handleTokenExpired);
60
+ return makeRequest(requestArgs).catch(this.handleNotOk);
59
61
  }
60
- handleTokenExpired = (response) => {
62
+ handleNotOk = async (response) => {
61
63
  if (response.status === 401) {
62
- this.onTokenExpired();
64
+ const parsed = await this.parseErrorResponse(response);
65
+ const errors = parsed?.errors || [];
66
+ const isTokenExpired = errors.some(e => /baboon/i.test(e.detail));
67
+ const isForceLogout = errors.some(e => /capuchin/i.test(e.detail));
68
+ isTokenExpired && this.onTokenExpired?.();
69
+ isForceLogout && this.onForceLogout?.();
63
70
  }
64
71
  return Promise.reject(response);
65
72
  };
73
+ parseErrorResponse = async (response) => {
74
+ try {
75
+ return (await response.json());
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ };
66
81
  get headers() {
67
82
  return {
68
83
  Accept: 'application/vnd.api+json',
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/utils/client/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiCollection, ApiResource } from '../../types'\nimport { Session } from '../session'\nimport { Uri } from '../uri'\nimport {\n concatRecords,\n ensureNoQueryParamsInDev,\n makeRequest,\n MakeRequestArgs,\n throwErrorIfFieldsMissing,\n throwErrorIfQueryParams,\n} from './request_helpers'\n\nimport { DeleteRequest, GetRequest, PatchRequest, PostRequest, WalkRequest } from './types'\n\ntype ClientArgs = {\n version: string\n defaultHeaders?: Record<string, string>\n onTokenExpired: () => void\n session: Session\n app: string\n}\n\nexport class Client {\n version: string = ''\n defaultHeaders: Record<string, string> = {}\n uri: Uri\n onTokenExpired: () => void\n\n constructor({ version, defaultHeaders = {}, session, app, onTokenExpired }: ClientArgs) {\n this.version = version\n this.uri = new Uri({ session, app })\n this.defaultHeaders = defaultHeaders\n this.onTokenExpired = onTokenExpired\n }\n\n async get<T extends ApiCollection | ApiResource>(args: GetRequest): Promise<T> {\n const { walk, ...data } = args.data\n const isWalking = Boolean(walk)\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n await throwErrorIfQueryParams(url)\n\n const requestArgs: MakeRequestArgs = { data, url, action: 'GET', headers }\n\n const walkRequest = ({\n url: requestUrl,\n data: d = { fields: {} },\n acc = { data: [], included: [], meta: {}, links: {} },\n ...options\n }: WalkRequest) => {\n return makeRequest({\n action: 'GET',\n data: d,\n url: requestUrl,\n ...options,\n headers,\n }).then(({ links, ...rest }) => {\n const records = Array.isArray(rest.data) ? concatRecords(acc, rest) : rest\n\n // `next` will have our params in the link so we do not want to pass them back in\n if (links?.next) {\n return walkRequest({ ...options, data: d, url: links.next, acc: records })\n } else {\n return records\n }\n })\n }\n\n const handler = isWalking ? walkRequest : makeRequest\n\n return throwErrorIfFieldsMissing(handler, requestArgs).catch(this.handleTokenExpired)\n }\n\n async patch(args: PatchRequest) {\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n const requestArgs: MakeRequestArgs = { data: args.data, url, action: 'PATCH', headers }\n\n return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleTokenExpired)\n }\n\n async post(args: PostRequest) {\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n const requestArgs: MakeRequestArgs = { ...args, data: args.data, url, action: 'POST', headers }\n\n return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleTokenExpired)\n }\n\n async delete(args: DeleteRequest) {\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n const requestArgs: MakeRequestArgs = { url, action: 'DELETE', headers }\n\n return makeRequest(requestArgs).catch(this.handleTokenExpired)\n }\n\n handleTokenExpired = (response: Response) => {\n if (response.status === 401) {\n this.onTokenExpired()\n }\n\n return Promise.reject(response)\n }\n\n get headers() {\n return {\n Accept: 'application/vnd.api+json',\n 'Content-Type': 'application/json',\n 'X-PCO-API-Version': this.version,\n ...this.uri.headers,\n ...this.defaultHeaders,\n }\n }\n}\n\nexport default Client\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/utils/client/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiCollection, ApiError, ApiResource } from '../../types'\nimport { Session } from '../session'\nimport { Uri } from '../uri'\nimport {\n concatRecords,\n ensureNoQueryParamsInDev,\n makeRequest,\n MakeRequestArgs,\n throwErrorIfFieldsMissing,\n throwErrorIfQueryParams,\n} from './request_helpers'\n\nimport { DeleteRequest, GetRequest, PatchRequest, PostRequest, WalkRequest } from './types'\n\ntype ClientArgs = {\n version: string\n defaultHeaders?: Record<string, string>\n onForceLogout?: () => void\n onTokenExpired?: () => void\n session: Session\n app: string\n}\n\nexport class Client {\n version: string = ''\n defaultHeaders: Record<string, string> = {}\n uri: Uri\n onTokenExpired?: () => void\n onForceLogout?: () => void\n\n constructor({\n version,\n defaultHeaders = {},\n session,\n app,\n onTokenExpired,\n onForceLogout,\n }: ClientArgs) {\n this.version = version\n this.uri = new Uri({ session, app })\n this.defaultHeaders = defaultHeaders\n this.onTokenExpired = onTokenExpired\n this.onForceLogout = onForceLogout\n }\n\n async get<T extends ApiCollection | ApiResource>(args: GetRequest): Promise<T> {\n const { walk, ...data } = args.data\n const isWalking = Boolean(walk)\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n await throwErrorIfQueryParams(url)\n\n const requestArgs: MakeRequestArgs = { data, url, action: 'GET', headers }\n\n const walkRequest = ({\n url: requestUrl,\n data: d = { fields: {} },\n acc = { data: [], included: [], meta: {}, links: {} },\n ...options\n }: WalkRequest) => {\n return makeRequest({\n action: 'GET',\n data: d,\n url: requestUrl,\n ...options,\n headers,\n }).then(({ links, ...rest }) => {\n const records = Array.isArray(rest.data) ? concatRecords(acc, rest) : rest\n\n // `next` will have our params in the link so we do not want to pass them back in\n if (links?.next) {\n return walkRequest({ ...options, data: d, url: links.next, acc: records })\n } else {\n return records\n }\n })\n }\n\n const handler = isWalking ? walkRequest : makeRequest\n\n return throwErrorIfFieldsMissing(handler, requestArgs).catch(this.handleNotOk)\n }\n\n async patch(args: PatchRequest) {\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n const requestArgs: MakeRequestArgs = { data: args.data, url, action: 'PATCH', headers }\n\n return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleNotOk)\n }\n\n async post(args: PostRequest) {\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n const requestArgs: MakeRequestArgs = { ...args, data: args.data, url, action: 'POST', headers }\n\n return ensureNoQueryParamsInDev(makeRequest, requestArgs).catch(this.handleNotOk)\n }\n\n async delete(args: DeleteRequest) {\n const headers = { ...this.headers, ...args.headers }\n const url = this.uri.appUrl(args.url)\n\n const requestArgs: MakeRequestArgs = { url, action: 'DELETE', headers }\n\n return makeRequest(requestArgs).catch(this.handleNotOk)\n }\n\n handleNotOk = async (response: Response) => {\n if (response.status === 401) {\n const parsed = await this.parseErrorResponse(response)\n const errors = parsed?.errors || []\n const isTokenExpired = errors.some(e => /baboon/i.test(e.detail))\n const isForceLogout = errors.some(e => /capuchin/i.test(e.detail))\n isTokenExpired && this.onTokenExpired?.()\n isForceLogout && this.onForceLogout?.()\n }\n\n return Promise.reject(response)\n }\n\n parseErrorResponse = async (response: Response): Promise<ApiError | undefined> => {\n try {\n return (await response.json()) as ApiError\n } catch {\n return undefined\n }\n }\n\n get headers() {\n return {\n Accept: 'application/vnd.api+json',\n 'Content-Type': 'application/json',\n 'X-PCO-API-Version': this.version,\n ...this.uri.headers,\n ...this.defaultHeaders,\n }\n }\n}\n\nexport default Client\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "2.2.2-rc.0",
3
+ "version": "2.3.0-rc.0",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -52,5 +52,5 @@
52
52
  "prettier": "^3.4.2",
53
53
  "typescript": "<5.6.0"
54
54
  },
55
- "gitHead": "85b57656095e4430283d8bcb345bf56875172a9f"
55
+ "gitHead": "a83eecfeb631cdd217b3be1d3836326d8db78943"
56
56
  }
@@ -3,7 +3,7 @@ import Client from '../utils/client/client'
3
3
  import { Session } from '../utils/session'
4
4
  import DefaultFixtures from '../__utils__/fixtures/defaults'
5
5
  import { BASE_URL } from '../__utils__/handlers'
6
- import { OAuthToken } from '../types'
6
+ import { ApiError, OAuthToken } from '../types'
7
7
 
8
8
  const APP_BASE_URL = BASE_URL
9
9
 
@@ -16,6 +16,9 @@ beforeAll(() => MockServer.server().listen())
16
16
  afterEach(() => MockServer.server().resetHandlers())
17
17
  afterAll(() => MockServer.server().close())
18
18
 
19
+ const onTokenExpired = jest.fn()
20
+ const onForceLogout = jest.fn()
21
+
19
22
  beforeEach(() => {
20
23
  jest.clearAllMocks()
21
24
  fetchSpy = jest.spyOn(globalThis, 'fetch')
@@ -24,7 +27,8 @@ beforeEach(() => {
24
27
  app: 'chat',
25
28
  version: '2018-11-01',
26
29
  session,
27
- onTokenExpired: () => {},
30
+ onTokenExpired,
31
+ onForceLogout,
28
32
  })
29
33
  clientWithDefaultHeaders = new Client({
30
34
  app: 'chat',
@@ -33,7 +37,8 @@ beforeEach(() => {
33
37
  'X-Custom-Default-Header': 'important data',
34
38
  },
35
39
  session,
36
- onTokenExpired: () => {},
40
+ onTokenExpired,
41
+ onForceLogout,
37
42
  })
38
43
  })
39
44
 
@@ -184,7 +189,12 @@ describe('get', () => {
184
189
  })
185
190
 
186
191
  it('throws an error if response is not ok', async () => {
187
- MockServer.get(url, { errors: [{ status: '403' }] }, 403, { once: true })
192
+ MockServer.get(
193
+ url,
194
+ { errors: [{ status: '403', detail: 'Zoinks, your token is borked' }] },
195
+ 403,
196
+ { once: true }
197
+ )
188
198
 
189
199
  await expect(
190
200
  client.get({
@@ -428,6 +438,51 @@ describe('url switching', () => {
428
438
  })
429
439
  })
430
440
 
441
+ describe('error handling', () => {
442
+ describe('401 errors', () => {
443
+ it('defaults', async () => {
444
+ await test401({ secretWord: '' })
445
+ expect(onTokenExpired).not.toHaveBeenCalled()
446
+ expect(onForceLogout).not.toHaveBeenCalled()
447
+ })
448
+
449
+ it('handles forced logout', async () => {
450
+ await test401({ secretWord: 'CAPUCHIN' })
451
+ expect(onTokenExpired).not.toHaveBeenCalled()
452
+ expect(onForceLogout).toHaveBeenCalled()
453
+ })
454
+
455
+ it('handles token refresh', async () => {
456
+ await test401({ secretWord: 'BABOON' })
457
+ expect(onTokenExpired).toHaveBeenCalled()
458
+ expect(onForceLogout).not.toHaveBeenCalled()
459
+ })
460
+
461
+ const test401 = async ({ secretWord }: { secretWord: string }) => {
462
+ const url = '/records'
463
+
464
+ const expiredTokenResource: Partial<ApiError> = {
465
+ errors: [
466
+ {
467
+ detail: `Zoinks, your token is borked ( ${secretWord} )`,
468
+ title: 'Unauthorized',
469
+ status: '401',
470
+ },
471
+ ],
472
+ }
473
+
474
+ MockServer.get(url, expiredTokenResource, 401, { once: true })
475
+
476
+ await expect(
477
+ client.get({
478
+ url,
479
+ data: { fields: { Record: ['id'] } },
480
+ })
481
+ ).rejects.toHaveProperty('status', 401)
482
+ }
483
+ })
484
+ })
485
+
431
486
  const requestHeadersShouldContain = ({
432
487
  headers,
433
488
  key,
@@ -1,6 +1,6 @@
1
1
  import { renderHook } from '@testing-library/react-native'
2
2
  import { useTheme } from '../../hooks'
3
- import { ChatProvider, CreateChatThemeProps } from '../../contexts'
3
+ import { ChatProvider, CreateChatThemeProps } from '../../contexts/chat_context'
4
4
  import React from 'react'
5
5
 
6
6
  let themeProps: CreateChatThemeProps = {
@@ -7,11 +7,14 @@ import { useConversationsJoltEvents } from '../hooks/use_conversation_jolt_event
7
7
  import { useConversations } from '../hooks/use_conversations'
8
8
  import { formatDatePreview } from '../utils/date'
9
9
  import { AvatarGroup, Badge, Heading, Text, TextButton } from './display'
10
+ import { ActionButton } from './display/action_button'
11
+ import { useCanCreateConversations } from '../hooks/use_chat_permissions'
10
12
 
11
13
  export const Conversations = () => {
12
14
  const styles = useStyles()
13
15
 
14
16
  const { conversations, fetchNextPage, refetch, isRefetching } = useConversations()
17
+ const canCreateConversations = useCanCreateConversations()
15
18
 
16
19
  // TODO: Filter using the API
17
20
  const nonEmptyConversations = conversations.filter(c => c.lastMessageTextPreview) || []
@@ -74,6 +77,11 @@ export const Conversations = () => {
74
77
  )}
75
78
  onEndReached={() => fetchNextPage()}
76
79
  />
80
+ <ActionButton
81
+ visible={canCreateConversations}
82
+ title="New conversation"
83
+ onPress={() => navigation.navigate('Create')}
84
+ />
77
85
  </View>
78
86
  )
79
87
  }
@@ -0,0 +1,62 @@
1
+ import React, { useState } from 'react'
2
+ import { Animated, LayoutAnimation, StyleSheet } from 'react-native'
3
+ import { Button } from './button'
4
+ import { useEffect } from 'react'
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
6
+ import { useTheme } from '../../hooks'
7
+ import { Text } from './text'
8
+
9
+ export const ActionButton = ({
10
+ visible = true,
11
+ onPress,
12
+ title,
13
+ infoText,
14
+ }: {
15
+ visible?: boolean
16
+ onPress: () => void
17
+ title: string
18
+ infoText?: string
19
+ }) => {
20
+ const styles = useStyles()
21
+ const [show, setShow] = useState(visible)
22
+
23
+ useEffect(() => {
24
+ if (show === visible) return
25
+
26
+ setShow(visible)
27
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
28
+ }, [show, visible])
29
+
30
+ if (!visible) return null
31
+
32
+ return (
33
+ <Animated.View style={styles.container}>
34
+ {Boolean(infoText) && (
35
+ <Text style={styles.infoText} variant="tertiary">
36
+ {infoText}
37
+ </Text>
38
+ )}
39
+ <Button variant="fill" size="lg" onPress={onPress} title={title} />
40
+ </Animated.View>
41
+ )
42
+ }
43
+
44
+ const useStyles = () => {
45
+ const { bottom } = useSafeAreaInsets()
46
+ const { colors } = useTheme()
47
+
48
+ return StyleSheet.create({
49
+ container: {
50
+ paddingVertical: 16,
51
+ paddingHorizontal: 24,
52
+ paddingBottom: bottom,
53
+ borderTopWidth: 1,
54
+ borderTopColor: colors.fillColorNeutral060,
55
+ gap: 16,
56
+ },
57
+ infoText: {
58
+ textAlign: 'center',
59
+ color: colors.textColorDefaultSecondary,
60
+ },
61
+ })
62
+ }
@@ -12,9 +12,9 @@ const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
12
12
  throw new Error('No token present')
13
13
  }
14
14
 
15
- const [url, data, headers] = queryKey as RequestQueryKey
15
+ const [url, data, headers, app = 'chat'] = queryKey as RequestQueryKey
16
16
 
17
- return apiClient.chat.get({ url, data, headers })
17
+ return apiClient[app].get({ url, data, headers })
18
18
  }
19
19
 
20
20
  export const queryClient = new QueryClient({
@@ -7,13 +7,14 @@ import { ChatTheme, defaultTheme, DefaultTheme } from '../utils/theme'
7
7
 
8
8
  export type ChatContextValue = {
9
9
  token?: OAuthToken
10
+ onForceLogout: () => void
10
11
  onTokenExpired: () => void
11
12
  theme: ChatTheme
12
13
  env?: ENV
13
14
  session: Session
14
15
  }
15
16
 
16
- export interface ChatProviderProps extends Omit<ChatContextValue, 'theme' | 'session'> {
17
+ export interface ChatProviderProps extends Omit<ChatContextValue, 'client' | 'theme' | 'session'> {
17
18
  theme: CreateChatThemeProps
18
19
  }
19
20
 
@@ -21,12 +22,13 @@ export const ChatContext = createContext<ChatContextValue>({
21
22
  theme: defaultTheme('light'),
22
23
  token: undefined,
23
24
  env: undefined,
25
+ onForceLogout: () => {},
24
26
  onTokenExpired: () => {},
25
27
  session: new Session(),
26
28
  })
27
29
 
28
30
  export function ChatProvider({ children, value }: { children: any; value: ChatProviderProps }) {
29
- const { env, token, onTokenExpired } = value
31
+ const { env, token, onTokenExpired, onForceLogout } = value
30
32
  const theme = useCreateChatTheme(value.theme || {})
31
33
  const session = useMemo(() => new Session({ token, env }), [env, token])
32
34
 
@@ -34,6 +36,7 @@ export function ChatProvider({ children, value }: { children: any; value: ChatPr
34
36
  env,
35
37
  token,
36
38
  onTokenExpired,
39
+ onForceLogout,
37
40
  session,
38
41
  theme,
39
42
  }
@@ -4,3 +4,5 @@ export * from './use_suspense_api'
4
4
  export * from './use_current_person'
5
5
  export * from './use_font_scale'
6
6
  export * from './use_create_android_ripple_color'
7
+ export * from './use_chat_permissions'
8
+ export * from './use_api_client'
@@ -0,0 +1,80 @@
1
+ import {
2
+ AnyUseSuspenseInfiniteQueryOptions,
3
+ InfiniteData,
4
+ useInfiniteQuery,
5
+ useQuery,
6
+ } from '@tanstack/react-query'
7
+ import { ApiCollection, ApiResource, ResourceObject } from '../types'
8
+ import { GetRequest, RequestData } from '../utils/client/types'
9
+ import { useApiClient } from './use_api_client'
10
+ import { getRequestQueryKey } from './use_suspense_api'
11
+
12
+ interface SuspenseGetOptions extends GetRequest {
13
+ app?: 'chat' | 'groups'
14
+ }
15
+
16
+ export const useApiGet = <T extends ResourceObject | ResourceObject[]>(
17
+ args: SuspenseGetOptions
18
+ ) => {
19
+ type Resource = ApiResource<T>
20
+
21
+ const { data, ...query } = useQuery<Resource, Response>({
22
+ queryKey: getRequestQueryKey(args),
23
+ })
24
+
25
+ return { ...data, ...query }
26
+ }
27
+
28
+ type NextMeta = Partial<{
29
+ offset: string
30
+ idLt: string
31
+ }>
32
+
33
+ export type SuspensePaginatorOptions = Omit<
34
+ AnyUseSuspenseInfiniteQueryOptions,
35
+ 'getNextPageParam' | 'initialPageParam' | 'queryFn' | 'queryKey'
36
+ >
37
+
38
+ export const useApiPaginator = <T extends ResourceObject>(
39
+ args: SuspenseGetOptions,
40
+ opts?: SuspensePaginatorOptions
41
+ ) => {
42
+ const apiClient = useApiClient()
43
+ const query = useInfiniteQuery<
44
+ ApiCollection<T>,
45
+ Response,
46
+ InfiniteData<ApiCollection<T>>,
47
+ any,
48
+ Partial<RequestData> | undefined
49
+ >({
50
+ queryKey: getRequestQueryKey(args),
51
+ queryFn: ({ pageParam }) => {
52
+ const pageParmWhere = pageParam?.where || {}
53
+ const argsWhere = args.data.where || {}
54
+ const where = { ...argsWhere, ...pageParmWhere }
55
+
56
+ const offset = pageParam?.offset || args.data.offset
57
+ const data = { ...args.data, where, offset }
58
+
59
+ return apiClient.chat.get({
60
+ url: args.url,
61
+ data,
62
+ })
63
+ },
64
+ initialPageParam: {} as Partial<RequestData>,
65
+ getNextPageParam: lastPage => {
66
+ const next: NextMeta = lastPage.meta?.next || {}
67
+ const { offset, idLt } = next
68
+
69
+ if (idLt) return { where: { id_lt: idLt } }
70
+ if (offset) return { offset: Number(offset) }
71
+
72
+ return undefined
73
+ },
74
+ ...(opts || {}),
75
+ })
76
+
77
+ const data: T[] = query.data?.pages.flatMap(page => page.data) || []
78
+
79
+ return { ...query, data }
80
+ }
@@ -2,26 +2,24 @@ import { useContext, useMemo } from 'react'
2
2
  import { ChatContext } from '../contexts/chat_context'
3
3
  import { Client } from '../utils/client'
4
4
 
5
- type App = 'chat' | 'groups'
6
- const apps: App[] = ['chat', 'groups']
7
- export type ApiClient = Record<App, Client>
5
+ type App = 'chat' | 'groups' | 'services'
6
+ const apps: App[] = ['chat', 'groups', 'services']
7
+
8
+ export type ApiClient = { [_K in App]: Client }
8
9
 
9
10
  export const useApiClient = () => {
10
11
  const { session, onTokenExpired } = useContext(ChatContext)
11
12
  const api = useMemo(
12
13
  () =>
13
- apps.reduce(
14
- (acc, app) => {
15
- acc[app] = new Client({
16
- app,
17
- session,
18
- version: '2018-11-01',
19
- onTokenExpired,
20
- })
21
- return acc
22
- },
23
- {} as Record<App, Client>
24
- ),
14
+ apps.reduce((acc, app) => {
15
+ acc[app] = new Client({
16
+ app,
17
+ session,
18
+ version: '2018-11-01',
19
+ onTokenExpired,
20
+ })
21
+ return acc
22
+ }, {} as ApiClient),
25
23
  [session, onTokenExpired]
26
24
  )
27
25
 
@@ -0,0 +1,20 @@
1
+ import { AppGrantsResource } from '../types'
2
+ import { useApiGet } from './use_api'
3
+
4
+ export function useAppGrants() {
5
+ return useApiGet<AppGrantsResource[]>({
6
+ url: '/me/app_grants',
7
+ data: {
8
+ fields: {
9
+ AppGrant: ['create_conversations', 'app_name'],
10
+ },
11
+ },
12
+ app: 'chat',
13
+ })
14
+ }
15
+
16
+ export function useCanCreateConversations(): boolean {
17
+ const { data: appGrants = [] } = useAppGrants()
18
+
19
+ return appGrants.some(appGrant => appGrant.createConversations)
20
+ }
@@ -1,10 +1,9 @@
1
- import { useMutation } from '@tanstack/react-query'
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
2
+ import { useState } from 'react'
2
3
  import { ApiResource, ConversationResource } from '../types'
3
- import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
4
- import { queryClient } from '../contexts'
5
- import { useApiClient } from './use_api_client'
6
4
  import { transformGetToPost } from '../utils/client/request_helpers'
7
- import { useState } from 'react'
5
+ import { useApiClient } from './use_api_client'
6
+ import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
8
7
 
9
8
  export const getConversationRequestArgs = ({ conversation_id }: { conversation_id: string }) => ({
10
9
  url: `/me/conversations/${conversation_id}`,
@@ -39,6 +38,7 @@ export const useConversation = ({ conversation_id }) => {
39
38
 
40
39
  export const useConversationMute = ({ conversation_id }: { conversation_id: string }) => {
41
40
  const apiClient = useApiClient()
41
+ const queryClient = useQueryClient()
42
42
  const requestArgs = getConversationRequestArgs({ conversation_id })
43
43
  const queryKey = getRequestQueryKey(requestArgs)
44
44
  const { data: conversation } = useConversation({ conversation_id })
@@ -82,6 +82,7 @@ export const useConversationMute = ({ conversation_id }: { conversation_id: stri
82
82
 
83
83
  export const useConversationUpdate = ({ conversation_id }: { conversation_id: string }) => {
84
84
  const apiClient = useApiClient()
85
+ const queryClient = useQueryClient()
85
86
  const requestArgs = getConversationRequestArgs({ conversation_id })
86
87
  const queryKey = getRequestQueryKey(requestArgs)
87
88
 
@@ -8,7 +8,13 @@ import { ApiCollection, ApiResource, ResourceObject } from '../types'
8
8
  import { GetRequest, RequestData } from '../utils/client/types'
9
9
  import { useApiClient } from './use_api_client'
10
10
 
11
- export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(args: GetRequest) => {
11
+ interface SuspenseGetOptions extends GetRequest {
12
+ app?: 'chat' | 'groups'
13
+ }
14
+
15
+ export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(
16
+ args: SuspenseGetOptions
17
+ ) => {
12
18
  type Resource = ApiResource<T>
13
19
 
14
20
  const { data, ...query } = useSuspenseQuery<Resource, Response>({
@@ -29,7 +35,7 @@ export type SuspensePaginatorOptions = Omit<
29
35
  >
30
36
 
31
37
  export const useSuspensePaginator = <T extends ResourceObject>(
32
- args: GetRequest,
38
+ args: SuspenseGetOptions,
33
39
  opts?: SuspensePaginatorOptions
34
40
  ) => {
35
41
  const apiClient = useApiClient()
@@ -72,9 +78,15 @@ export const useSuspensePaginator = <T extends ResourceObject>(
72
78
  return { ...query, data }
73
79
  }
74
80
 
75
- export type RequestQueryKey = [GetRequest['url'], GetRequest['data'], GetRequest['headers']]
76
- export const getRequestQueryKey = (args: GetRequest): RequestQueryKey => [
81
+ export type RequestQueryKey = [
82
+ SuspenseGetOptions['url'],
83
+ SuspenseGetOptions['data'],
84
+ SuspenseGetOptions['headers'],
85
+ SuspenseGetOptions['app'],
86
+ ]
87
+ export const getRequestQueryKey = (args: SuspenseGetOptions): RequestQueryKey => [
77
88
  args.url,
78
89
  args.data,
79
90
  args.headers,
91
+ args.app || 'chat',
80
92
  ]